在 Access 实现标签输入控件:VBA + HTML 混合开发实战

Hi,大家好!

做过 Web 开发的人一定见过这种交互:一个输入框,输入文字后按回车,文字变成一个带 × 号的小标签,可以继续添加,可以点击删除。这种控件叫 Input Tags(标签输入),在 Web 端几乎是标配。

但在 Access 窗体里,原生控件根本没有这个东西。文本框只能显示扁平字符串,组合框只能选一个值,列表框多选体验又差。

今天分享一个完整的解决方案:在 Access 窗体中嵌入一个 HTML 标签输入控件,用户像在网页上一样添加和删除标签,数据自动存回数据库。

先看效果:

一、整体方案

Access 窗体有一个很多人不知道的控件:Microsoft Web Browser,本质上是一个嵌入式 IE 浏览器。

我们的思路是:

  1. 用纯 HTML/CSS/JS 写一个标签输入控件
  2. 通过 WebBrowser 控件把它嵌入 Access 窗体
  3. VBA 和 JS 之间通过 execScript + 隐藏元素交换数据
项目 说明
使用控件 Microsoft Web Browser(ActiveX,IE 内核)
不用 Edge 的原因 Edge 控件不支持 VBA↔JS 双向通信(没有 execScript,没有 DOM 访问)
前端页面 tag_editor.html(纯手写,无外部依赖,IE11 兼容)
数据交换 VBA→JS:execScript 调用 JS 函数;JS→VBA:DOM 读取隐藏元素 #bridge

二、制作步骤

步骤 1:准备 HTML 文件

tag_editor.html 放到你的 .accdb 文件所在的同一个文件夹下。

这个 HTML 页面是一个纯手写的标签输入控件,不依赖任何外部库,完全兼容 IE11。支持的交互包括:

  • 回车、英文逗号、中文逗号添加标签
  • 点击 × 删除标签
  • Backspace 快速删除最后一个标签
  • 自动去重

HTML 源码见文末。

步骤 2:在窗体中插入控件

设计视图打开你的窗体,需要添加三个东西:

① WebBrowser 控件

功能区选择 设计ActiveX 控件 → 找到 Microsoft Web Browser,拖放到窗体上。

  • 控件名称保持默认的 WebBrowser0
  • 建议宽度与窗体等宽,高度 40--60

② 隐藏文本框

添加一个文本框,命名为 txtTagsData

  • 控件来源绑定到数据表中存储标签的字段(例如"标签"字段)
  • 可见 属性设为

这个文本框是 VBA 和数据库之间的桥梁,用户不需要看到它。

③ 保存按钮

添加一个命令按钮,命名为 btnSave,标题设为"保存标签"。

步骤 3:粘贴 VBA 代码

在窗体上右键 → 生成代码(或按 Alt+F11),打开 VBA 编辑器。删除已有的所有代码,粘贴完整的 VBA 代码。

vb 复制代码
Option Compare Database
Option Explicit

' 标记网页是否加载完成
Private mBrowserReady As Boolean

' ==============================================
' 窗体打开:导航到本地 HTML
' ==============================================
Private Sub Form_Load()
    mBrowserReady = False

    Dim htmlPath As String
    htmlPath = CurrentProject.path & "\tag_editor.html"

    Me.WebBrowser0.Navigate htmlPath
End Sub

' ==============================================
' 网页加载完成(WebBrowser 控件原生事件)
' ==============================================
Private Sub WebBrowser0_DocumentComplete(ByVal pDisp As Object, URL As Variant)
    mBrowserReady = True
    SyncTagsToEditor
End Sub

' ==============================================
' 切换记录时,把数据库中的标签推送到网页
' ==============================================
Private Sub Form_Current()
    If mBrowserReady Then
        SyncTagsToEditor
    End If
End Sub

' ==============================================
' 保存按钮:从网页读取标签并存入数据库
' ==============================================
Private Sub btnSave_Click()
    If Not mBrowserReady Then
        MsgBox "编辑器尚未加载完成,请稍候。", vbExclamation
        Exit Sub
    End If

    Dim tagsData As String
    tagsData = ReadTagsFromBrowser()

    Me.txtTagsData.value = tagsData

    If Me.Dirty Then Me.Dirty = False

    MsgBox "标签已保存:" & tagsData, vbInformation
End Sub

' ==============================================
' 辅助:将数据库中的标签推送到网页编辑器
' ==============================================
Private Sub SyncTagsToEditor()
    On Error Resume Next

    Dim currentTags As String
    currentTags = Nz(Me.txtTagsData.value, "")
    currentTags = EscapeForJs(currentTags)

    Me.WebBrowser0.Document.parentWindow.execScript _
        "setTags('" & currentTags & "');", "JavaScript"

    On Error GoTo 0
End Sub

' ==============================================
' 辅助:从网页读取标签
'   原理:先调用 JS 的 writeBridge() 将标签写入
'         隐藏的 <input id="bridge">,再用 DOM 读取
' ==============================================
Private Function ReadTagsFromBrowser() As String
    On Error GoTo Failed

    ' 让 JS 把标签写入 bridge 元素
    Me.WebBrowser0.Document.parentWindow.execScript _
        "writeBridge();", "JavaScript"

    ' 通过 DOM 读取 bridge 元素的值
    Dim bridgeValue As String
    bridgeValue = Nz(Me.WebBrowser0.Document.getElementById("bridge").value, "")

    ReadTagsFromBrowser = bridgeValue
    Exit Function

Failed:
    ' 如果读取失败,回退到文本框当前值
    ReadTagsFromBrowser = Nz(Me.txtTagsData.value, "")
End Function

' ==============================================
' 辅助:将 VBA 字符串转义后嵌入 JS 单引号字符串
' ==============================================
Private Function EscapeForJs(ByVal s As String) As String
    s = Replace(s, "\", "\\")
    s = Replace(s, "'", "\'")
    s = Replace(s, vbCrLf, "")
    s = Replace(s, vbCr, "")
    s = Replace(s, vbLf, "")
    EscapeForJs = s
End Function

步骤 4:设置事件属性

回到窗体设计视图,在属性表中确认以下事件已关联:

对象 属性
窗体 On Load [Event Procedure]
窗体 On Current [Event Procedure]
WebBrowser0 On Document Complete [Event Procedure]
btnSave On Click [Event Procedure]

如果下拉列表中看不到 [Event Procedure],选中它后点击右边的 ... 按钮即可。

步骤 5:运行测试

切换到窗体视图,你应该能看到:

  1. WebBrowser 区域自动加载标签编辑器
  2. 输入文字,按回车或逗号,变成标签
  3. 点击标签上的 × 可以删除
  4. 点击"保存标签"按钮,数据存入数据库
  5. 切换记录时,标签编辑器自动恢复对应的标签

三、几个关键技术点

做出来不难,但有几个技术细节值得了解。

1. 安全警告的处理

如果直接用 Navigate 加载本地 HTML 文件,IE 内核会弹安全警告。

本方案采用注入式加载 :先导航到 about:blank,再用 VBA 读取 HTML 文件内容,通过 Document.Write 注入。浏览器始终停留在 about:blank,属于本地安全区域,不会触发任何警告。

2. 加载时序

WebBrowser 的 DocumentComplete 事件会触发两次:第一次是 about:blank 加载完成(此时注入 HTML),第二次是注入的 HTML 渲染完成(此时才能调用 JS)。

代码里用两个布尔变量 mWaitingInjectmBrowserReady 做状态机,区分这两次触发,避免时序错乱。

3. VBA 与 JS 的双向数据交换

方向 实现方式
VBA → JS execScript 调用 setTags('标签1,标签2')
JS → VBA 先让 JS 把数据写入隐藏的 <input id="bridge">,再用 DOM 读取

execScript 是单向的,没有返回值。所以从 JS 向 VBA 传数据,需要借助一个隐藏 HTML 元素做"桥梁"------这就是代码里 Bridge 模式的由来。

4. 字符串转义

VBA 拼接字符串传给 JS 时,如果标签内容包含单引号、反斜杠或换行符,会导致 JS 语法错误。代码里有一个 EscapeForJs 函数专门处理这个问题。

5. 中文逗号兼容

keydown 事件能捕获英文逗号,但中文逗号是通过输入法输入的,keydown 拿不到。前端代码在 input 事件中检测中文逗号(Unicode \uff0c),按逗号分割后逐个添加。

四、数据流全貌

整个数据流是闭环的:

复制代码
数据库 → 隐藏文本框 txtTagsData → VBA execScript → JS setTags() → 页面渲染标签
                                                                         ↓
                                                                    用户编辑
                                                                         ↓
数据库 ← 隐藏文本框 txtTagsData ← VBA DOM 读取 ← JS writeBridge() ← 点击保存

标签以逗号分隔的字符串存入数据库,例如:Access,VBA,数据库

如果不想让用户手动点保存按钮,可以在窗体的 BeforeUpdate 事件中自动读取标签写入文本框,实现无感保存。

五、这套方法还能做什么

抛开标签输入这个具体功能,这个项目展示的其实是一套通用框架:在 Access 窗体中通过 WebBrowser 嵌入任意 HTML 界面。

同样的思路可以做:

场景 嵌入的内容
富文本编辑器 contentEditable 编辑器
颜色选择器 HTML5 color picker
评分控件 五星点击评分
签名板 Canvas 手写签名
甘特图 轻量级时间线渲染

核心套路就四步:

  1. about:blank + Document.Write 注入 HTML
  2. 状态机控制加载时序
  3. execScript + Bridge 元素实现双向通信
  4. 隐藏文本框做数据库绑定

掌握这套方法,Access 窗体的 UI 能力就不再局限于原生控件了。

总结

这个项目要解决的问题很具体:在 Access 窗体里做一个标签输入控件。

但它涉及的技术点串在一起,其实是一套完整的混合开发方案:

  • 控件选型:为什么用 IE WebBrowser 而不是 Edge WebView2
  • 安全规避about:blank 注入式加载避免脚本警告
  • 时序控制 :双触发 DocumentComplete 的状态机处理
  • 数据通信execScript + Bridge 桥接的双向协议
  • 前端兼容:纯 IE11 手写控件,无外部依赖
  • 数据闭环:隐藏文本框 + 窗体事件实现自动同步

这些技术点单独拿出来都不复杂,但组合在一起解决了一个 Access 原生做不到的交互需求。

用合适的方式把问题拆开,再一个个解决------这也许就是工程开发最本质的样子。

附录:源码

tag_editor.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Access Tag Editor</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
            font-size: 13px;
            padding: 4px;
            background: transparent;
        }

        /* ========== 标签容器 ========== */
        .tag-container {
            display: -ms-flexbox;
            display: flex;
            -ms-flex-wrap: wrap;
            flex-wrap: wrap;
            -ms-flex-align: center;
            align-items: center;
            border: 1px solid #adb5bd;
            border-radius: 4px;
            padding: 3px;
            min-height: 30px;
            cursor: text;
            background: #fff;
        }
        .tag-container.focused {
            border-color: #0078d4;
            box-shadow: 0 0 0 1px rgba(0,120,212,.4);
        }

        /* ========== 单个标签 ========== */
        .tag-item {
            display: -ms-inline-flexbox;
            display: inline-flex;
            -ms-flex-align: center;
            align-items: center;
            background: #e1ecf4;
            color: #39739d;
            border-radius: 3px;
            padding: 2px 4px 2px 7px;
            margin: 2px;
            font-size: 13px;
            line-height: 1.5;
            white-space: nowrap;
        }

        .tag-text { margin-right: 3px; }

        .tag-close {
            display: inline-block;
            width: 16px;
            height: 16px;
            line-height: 16px;
            text-align: center;
            cursor: pointer;
            font-size: 15px;
            color: #7aa7c7;
            border-radius: 50%;
        }
        .tag-close:hover {
            background: #c0392b;
            color: #fff;
        }

        /* ========== 输入框 ========== */
        .tag-input {
            border: none;
            outline: none;
            -ms-flex: 1;
            flex: 1;
            min-width: 80px;
            font-size: 13px;
            padding: 3px 4px;
            font-family: inherit;
            background: transparent;
        }
    </style>
</head>
<body>
    <div class="tag-container" id="tagContainer">
        <input type="text" class="tag-input" id="tagInput" placeholder="输入标签, 按回车或逗号添加...">
    </div>

    <!-- 隐藏元素:供 VBA 通过 DOM 读写数据 -->
    <input type="hidden" id="bridge" value="">

    <script>
        // ============================================================
        //  纯 IE11 兼容写法 ------ 无 ES6+,无外部依赖
        // ============================================================

        var container = document.getElementById("tagContainer");
        var input     = document.getElementById("tagInput");
        var bridge    = document.getElementById("bridge");
        var tags      = [];

        // ------ 事件绑定 ------

        container.onclick = function (e) {
            if (e.target === container) { input.focus(); }
        };

        input.onfocus = function () { container.className = "tag-container focused"; };
        input.onblur  = function () { container.className = "tag-container"; };

        input.onkeydown = function (e) {
            var key = e.keyCode || e.which;
            var val = input.value.replace(/,/g, "").replace(/^\s+|\s+$/g, "");

            // 回车 (13) 或英文逗号 (188)
            if (key === 13 || key === 188) {
                if (e.preventDefault) { e.preventDefault(); }
                e.returnValue = false;
                if (val !== "") {
                    addTag(val);
                    input.value = "";
                }
            }

            // Backspace (8) --- 输入框为空时删除最后一个标签
            if (key === 8 && input.value === "" && tags.length > 0) {
                removeTagByIndex(tags.length - 1);
            }
        };

        // 处理中文逗号
        if (input.addEventListener) {
            input.addEventListener("input", handleChineseComma, false);
        } else if (input.attachEvent) {
            input.attachEvent("oninput", handleChineseComma);
        }

        function handleChineseComma() {
            var v = input.value;
            if (v.indexOf("\uff0c") !== -1) {           // \uff0c = ,
                var parts = v.split("\uff0c");
                for (var i = 0; i < parts.length - 1; i++) {
                    var t = parts[i].replace(/^\s+|\s+$/g, "");
                    if (t !== "") { addTag(t); }
                }
                input.value = parts[parts.length - 1];
            }
        }

        // ------ 核心函数 ------

        function addTag(text) {
            text = text.replace(/^\s+|\s+$/g, "");
            if (text === "") { return; }
            for (var i = 0; i < tags.length; i++) {
                if (tags[i] === text) { return; }       // 去重
            }
            tags.push(text);
            renderTags();
        }

        function removeTagByIndex(index) {
            tags.splice(index, 1);
            renderTags();
        }

        function renderTags() {
            var existing = container.querySelectorAll(".tag-item");
            for (var i = existing.length - 1; i >= 0; i--) {
                container.removeChild(existing[i]);
            }
            for (var j = 0; j < tags.length; j++) {
                var el = document.createElement("span");
                el.className = "tag-item";
                el.setAttribute("data-index", j);

                var textSpan = document.createElement("span");
                textSpan.className = "tag-text";
                textSpan.innerText = tags[j];
                el.appendChild(textSpan);

                var closeBtn = document.createElement("span");
                closeBtn.className = "tag-close";
                closeBtn.innerText = "\u00d7";          // ×
                closeBtn.setAttribute("data-index", j);
                closeBtn.onclick = function () {
                    var idx = parseInt(this.getAttribute("data-index"), 10);
                    removeTagByIndex(idx);
                };
                el.appendChild(closeBtn);

                container.insertBefore(el, input);
            }
            bridge.value = tags.join(",");
        }

        // ============================================================
        //  供 Access VBA 通过 execScript 调用的全局函数
        // ============================================================

        /** 获取所有标签(逗号分隔字符串) */
        function getTags() {
            return tags.join(",");
        }

        /** 设置 / 加载标签(逗号分隔字符串) */
        function setTags(tagsStr) {
            tags = [];
            if (tagsStr && tagsStr !== "") {
                var arr = tagsStr.split(",");
                for (var i = 0; i < arr.length; i++) {
                    var t = arr[i].replace(/^\s+|\s+$/g, "");
                    if (t !== "") { tags.push(t); }
                }
            }
            renderTags();
        }

        /** 将标签写入 bridge 元素(供 VBA 读取) */
        function writeBridge() {
            bridge.value = getTags();
        }
    </script>
</body>
</html>
相关推荐
程序员一点2 小时前
第23章:备份与灾难恢复策略
linux·运维·网络·数据库·openeuler
数据知道2 小时前
MongoDB内存使用优化:working set理论与缓存命中率提升策略
数据库·mongodb
૮・ﻌ・2 小时前
Nodejs - 02:模块化、npm、yarn、cnpm
前端·npm·node.js·express·yarn·cnpm·包管理工具
大雷神2 小时前
HarmonyOS APP<玩转React>开源教程十:组件化开发概述
前端·react.js·开源·harmonyos
SelectDB技术团队2 小时前
OLAP 无需事务?Apache Doris 如何让实时分析兼具事务保障
数据库·数据仓库·人工智能·云原生·实时分析
数据库小组2 小时前
NineData 社区版慢 SQL 功能能做什么?给 DBA 的一套本地化治理工具
数据库·sql·dba·慢sql·数据库管理工具·ninedata·迁移工具
胡斌附体2 小时前
配置导入事务问题与修复总结
excel·导入·spring事务·吞异常·使用独立事务
老友@2 小时前
微服务全面解析:架构、组件与底层原理
数据库·spring·oracle
小小小小宇2 小时前
React useMemo 深度源码解析
前端