QML TextArea 实战(中):Markdown 渲染与保持滚动

这两个 demo 都围绕"内容动态变化的文本区"展开:一个是输入时左侧 Markdown 实时渲染右侧预览,一个是点击按钮追加文本时滚动条自动跟随到底端。两者的共同难点是处理"内容增长后视图怎么响应"------一个靠 Flickable 同步滚动,一个靠 ScrollBar.position 声明式绑定。

这篇介绍两个 demo:

  • MarkDown 编辑器 --- 左右分栏,左侧输入 Markdown,右侧用 Text.MarkdownText 实时渲染
  • 保持滚动 --- 点击按钮追加文本,垂直滚动条 position 始终保持在底端

MarkDown 编辑器

这个 demo 做一个简易 Markdown 编辑器:左侧 TextArea 输入 Markdown 源文,右侧 TexttextFormat: Text.MarkdownText 实时渲染。外层用 Flickable + SplitView 解决两栏同步滚动。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts

// MarkDown编辑器:左侧输入 Markdown,右侧实时预览渲染结果
FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10

        TitleSeparator {
            title: "MarkDown 编辑器"
            description: "左侧输入 Markdown 文本,右侧以 Text.MarkdownText 实时渲染预览"
        }

        // 解决了同步滚动的问题
        Flickable {
            Layout.fillWidth: true
            Layout.fillHeight: true
            contentWidth: parent.width
            contentHeight: Math.max(markdownPreview.implicitHeight,
                                    markdownInput.implicitHeight,
                                    parent.height)
            clip: true
            ScrollBar.vertical: ScrollBar {}

            SplitView {
                anchors.fill: parent

                // Markdown 输入区域
                TextArea {
                    id: markdownInput
                    SplitView.preferredWidth: parent.width / 2
                    placeholderText: "输入 Markdown 文本..."
                    font.family: "Courier New"
                    wrapMode: TextArea.Wrap

                    background: Rectangle {
                        color: markdownInput.enabled ? "white" : "#f5f5f5"
                        border.color: markdownInput.activeFocus ? "#1296FF" : "#c0c0c0"
                        border.width: markdownInput.activeFocus ? 2 : 1
                        radius: 4
                    }
                }

                // Markdown 预览区域
                Text {
                    id: markdownPreview
                    SplitView.fillWidth: true
                    wrapMode: Text.Wrap
                    textFormat: Text.MarkdownText
                    text: markdownInput.text
                    padding: 10
                    onLinkActivated: Qt.openUrlExternally(link)
                }
            }
        }
    }
}

关键逻辑解析

Text.MarkdownText:原生 Markdown 渲染

Qt 6 的 Text 控件内置 Markdown 支持,把 textFormat 设成 Text.MarkdownText 即可渲染 CommonMark 子集:

  • 标题:#######
  • 强调:**加粗***斜体*~~删除线~~
  • 列表:- 无序列表1. 有序列表
  • 链接:[文本](url)
  • 代码:行内代码代码块
  • 引用:> 引用块
  • 分隔线:---

相比第 1 篇用 TextArea.RichText 解析 HTML 子集,MarkdownText 直接吃 Markdown 源文,不用先转 HTML,写起来更直接。前提是 Qt 版本 ≥ 6(早期 5.x 需要 QtMarkdown 第三方库)。

左右双栏的实时联动

qml 复制代码
text: markdownInput.text

右侧 Texttext 绑定到左侧 TextArea.text,输入即渲染。这是声明式绑定的典型用法------不需要 onTextChanged 信号手动同步,QML 绑定机制自动处理。

Flickable 解决两栏同步滚动

两栏内容长度不一致时(Markdown 源文短、渲染后可能更长,或反过来),需要同步滚动。这个 demo 的做法是把 SplitView 放进 Flickable 里:

  • contentHeightmarkdownPreview.implicitHeightmarkdownInput.implicitHeightparent.height 三者最大值,保证 Flickable 的内容区能容纳较长的一栏。
  • 滚动 Flickable 时,SplitView 整体上下移动,两栏同步滚动。
  • ScrollBar.vertical: ScrollBar {}Flickable 加可见滚动条。

这是"整体滚动"思路------两栏作为整体一起滚。如果要做"两栏独立滚动但位置同步"(像 Typora 的编辑预览模式),需要在 onContentYChanged 里手动同步另一栏的 contentY,复杂度高很多。

SplitView 的分栏

SplitView 让两栏可拖拽分隔条调整宽度:

  • SplitView.preferredWidth: parent.width / 2:左侧初始占一半。
  • SplitView.fillWidth: true:右侧填满剩余宽度。

拖动中间分隔条会动态调整两栏比例,适合"编辑/预览"场景------写代码时拉宽输入区,看效果时拉宽预览区。

onLinkActivated 处理链接点击

qml 复制代码
onLinkActivated: Qt.openUrlExternally(link)

Markdown 里的 [文本](url) 渲染成可点击链接,点击时触发 onLinkActivated 信号,Qt.openUrlExternally(link) 调系统默认浏览器打开链接。不加这个处理,链接点了没反应。

font.family: "Courier New"

输入区用等宽字体,让 Markdown 的 #-* 等符号对齐,写起来更像代码编辑器。预览区用默认字体,渲染后的标题、正文各有样式。

适用场景

任何需要"编辑 + 预览"双模式的场景:

  • 笔记应用:输入 Markdown,预览排版效果
  • 文档编辑器:写 README、API 文档时实时预览
  • 富文本输入:用 Markdown 语法代替工具栏按钮,输入更快

把输入区换成 TextField + 自动补全,能做命令行式的 Markdown 快速输入。

保持滚动

这个 demo 模拟日志输出场景:点击按钮追加文本,垂直滚动条 position 始终保持在底端,新内容自动滚动到可视区域。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts

// 保持滚动:点击按钮追加文本,垂直滚动条始终保持滚动到底端
FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10

        TitleSeparator {
            title: "保持滚动"
            description: "点击按钮追加文本,垂直滚动条 position 始终保持在底端"
        }

        RowLayout {
            Layout.fillWidth: true
            spacing: 10

            Button {
                text: "追加文本"
                onClicked: {
                    textArea.append("【追加文本】这是一个测试文本,用于演示滚动功能。\n".repeat(5))
                }
            }

            Label {
                text: "已追加文本,字符数: " + textArea.text.length
                color: "#666"
                font.pixelSize: 12
            }
        }

        ScrollView {
            id: scrollView
            Layout.fillWidth: true
            Layout.fillHeight: true

            ScrollBar.vertical: ScrollBar {
                anchors.top: scrollView.top
                anchors.bottom: scrollView.bottom
                anchors.right: scrollView.right
                policy: ScrollBar.AlwaysOn

                // 保持滚动到底端
                position: {
                    if (scrollView.height > 0 && scrollView.contentHeight > scrollView.height) {
                        return (scrollView.contentHeight - scrollView.height) / scrollView.contentHeight;
                    }
                    return 0;
                }
            }

            TextArea {
                id: textArea
                wrapMode: TextArea.Wrap
                text: "这是一个测试文本,用于演示滚动功能。\n".repeat(30)
                selectByMouse: true
                mouseSelectionMode: TextEdit.SelectCharacters

                background: Rectangle {
                    color: textArea.enabled ? "white" : "#f5f5f5"
                    border.color: textArea.activeFocus ? "#1296FF" : "#c0c0c0"
                    border.width: textArea.activeFocus ? 2 : 1
                    radius: 4
                }
            }
        }
    }
}

关键逻辑解析

TextArea.append() 追加文本

qml 复制代码
textArea.append("【追加文本】这是一个测试文本,用于演示滚动功能。\n".repeat(5))

append(text)TextArea 的方法,在末尾追加文本并自动换行。和直接改 text 属性的区别:

  • text = text + newText:整个 text 重新赋值,文本很长时性能差
  • append(newText):内部只处理追加部分,性能更好,适合日志类持续追加场景

.repeat(5) 是 JS 字符串方法,把文本重复 5 次模拟一段较长输出。

ScrollBar.position 声明式绑定

核心逻辑在 ScrollBar.vertical 里:

qml 复制代码
position: {
    if (scrollView.height > 0 && scrollView.contentHeight > scrollView.height) {
        return (scrollView.contentHeight - scrollView.height) / scrollView.contentHeight;
    }
    return 0;
}

position 是滚动条当前位置,取值 0 到 1 - sizesize 是可见部分占比)。把 position 绑定到一个表达式,让滚动条始终指向底端:

  • scrollView.contentHeight:内容总高度
  • scrollView.height:可视区高度
  • (contentHeight - height) / contentHeight:底端位置------内容超出可视区时,这个值就是滚动到底部所需的 position

这是声明式绑定------contentHeightheight 变化时,position 自动重算。追加文本后 contentHeight 增大,position 跟着变大,滚动条自动滑到底端。

为什么不用 onContentHeightChanged 信号

也可以写成命令式:

qml 复制代码
onContentHeightChanged: {
    if (contentHeight > height) {
        scrollBar.position = (contentHeight - height) / contentHeight
    }
}

但声明式绑定更简洁,也避免了"信号触发顺序"的坑。QML 里能用绑定就用绑定,需要复杂计算或副作用时才用信号。

policy: ScrollBar.AlwaysOn

滚动条始终显示,不自动隐藏。日志场景下用户需要持续看到滚动条位置,AlwaysOnAsNeeded(按需显示)更合适。

anchors 固定滚动条位置

qml 复制代码
anchors.top: scrollView.top
anchors.bottom: scrollView.bottom
anchors.right: scrollView.right

ScrollBar 默认会自动定位,但显式 anchors 能确保滚动条贴在 ScrollView 右侧并占满高度,避免某些主题下位置偏移。

适用场景

任何"持续输出内容 + 自动跟随到底部"的场景:

  • 日志查看器:后台持续打印日志,滚动条始终在底部
  • 聊天窗口:新消息追加后自动滚动到最新
  • 控制台输出:命令行输出实时显示
  • 构建日志:编译过程输出持续追加

反过来,如果用户手动往上滚动查看历史,应该停止自动跟随------这时需要加个标志位判断"用户是否在底部",只有靠近底部时才自动滚动。

两个 demo 对比

维度 MarkDown 编辑器 保持滚动
内容变化方式 输入框实时编辑 按钮触发 append()
视图响应 Flickable 整体同步滚动 ScrollBar.position 绑定到底端
核心机制 Text.MarkdownText 渲染 声明式属性绑定
典型场景 编辑 + 预览双栏 日志、聊天持续输出

两个 demo 分别代表"内容编辑"和"内容追加"两种动态文本场景。前者靠 Flickable 同步两栏滚动,后者靠 ScrollBar.position 绑定让滚动条自动跟随。理解了这两种模式,遇到"内容动态变化的文本区"基本能举一反三。

运行验证

  1. Qt Creator 打开 qml_textarea/CMakeLists.txt
  2. Ctrl+R 运行
  3. 在左侧导航切换"MarkDown 编辑器"、"保持滚动"两个 demo

扩展复用方向

  • MarkDown 编辑器加"导出 HTML"按钮:用 markdownPreview.text 拿到渲染后的 HTML,写入文件
  • MarkDown 编辑器加语法高亮:输入区换成支持 Markdown 语法高亮的组件(需要自己实现或用第三方库)
  • 保持滚动加"用户滚动检测":监听 ScrollBar.position 手动变化,用户往上滚时停止自动跟随,回到底部后恢复
  • 保持滚动改成"追加后平滑动画滚动到底部":用 NumberAnimation 动画 position 从当前值到 1,过渡更自然
  • 把保持滚动封装成 LogView.qml 组件,提供 append(text) 方法,外部只管追加内容,滚动跟随逻辑内部处理

已验证环境

相关推荐
Quz2 小时前
QML TextArea 实战(下):大文本加载与性能优化
qt
C++ 老炮儿的技术栈17 小时前
基于Qt实现轻量化本地音乐播放器
开发语言·c++·qt·c·播放器·音乐
我不是疯子是傻子21 小时前
Qt 实时曲线卡顿优化:从QPainter到OpenGL的3级加速实战
开发语言·qt
秋田君1 天前
QT_XML文件操作
xml·数据库·qt
我不是疯子是傻子1 天前
串口通信数据帧粘包拆包再进化:基于 Qt 的滑动窗口与 CRC 校验实战
开发语言·qt
程序员老陆1 天前
Qt中实现窗口真全屏(覆盖Windows任务栏)
开发语言·windows·qt
艾莉丝努力练剑1 天前
【QT:解决问题】Qt5Core.dll:无法定位程序输入点
java·开发语言·qt·学习·面试
秋田君2 天前
QT_JSON文件操作
开发语言·qt·json
大白同学4212 天前
初识Qt——信号和槽
开发语言·qt