Access 存图片别用「插入对象」:ADO Stream + AppendChunk 原格式保存

3.png

摘要: 一张 200KB 的 JPG,用"插入对象"存进 Access,可能变成 2MB。图片一多,数据库肥得没法看。这篇讲一个老办法:用 ADO Stream 读取图片,再用 AppendChunk 写入 OLE 对象字段,按原格式保存并还原预览。方法不新,但确实好用。access开发|access培训|access框架|请添加edonsoft。

Hi,大家好!

前阵子有个朋友找我,说他做的产品档案系统越用越慢,库文件三百多 MB,打开要等半天。他挺纳闷:总共也就几百条产品记录,一条记录一张图,怎么会这么大?

我远程看了一眼,问题出在图片的存法上——他是在绑定对象框里右键"插入对象",一张一张把图片插进去的。

这个操作本身没毛病,Access 就是这么设计的。坑在后面:JPG、PNG 这类图片经过"插入对象"存进去,会被包装成 OLE 对象,体积直接翻好几倍。更早的 Access 版本干脆把图转成位图,一张 200KB 的照片进去,出来就是几 MB。

后来我帮他把存图的方式改了改,别的什么都没动,再压缩修复一下,300 多 MB 变成不到 50MB。那今天我们就来看看我是怎么实现的。

一、先看你的图适不适合存库里

图片能不能存进 Access?能。该不该存?得看情况。

头像、产品主图、证件照这类,一条记录一两张、单张几百 KB 的,存库里没问题,还有个好处——整个系统就一个文件,拷走就能用,不用担心图片文件夹丢了、路径变了。

但要是巡检照片、施工现场图、高清扫描件这种,一条记录挂十几张、动不动一张好几 MB 的,就别往库里塞了。accdb 有 2GB 的硬上限,这么存撑不了多久。我就见过一个巡检系统,上线不到半年库就顶到头了,最后只能连夜改造。这种场景应该图片放文件夹或者服务器,库里只记路径和文件名。

一句话:图少存库里,图多存路径。今天讲的方法属于前一种。

二、思路:绕开 OLE 包装,直接存文件本身

"插入对象"为什么会让图片变大?因为 Access 存的不是图片文件本身,而是一个 OLE 对象——除了图片数据,里面还带了一堆用来支持"双击可编辑"的包装信息。2007 之前的版本更夸张,会把图片转成未压缩的位图,JPG 辛辛苦苦压缩的那点体积优势直接归零。

那就换个思路:不让 Access 碰这张图,把它当成一个普通文件,用 ADO 的 Stream 对象按二进制原样读进来,写到表字段里。文件多大,存进去就多大,一个字节都不多。

显示的时候反过来:把字段里的二进制读出来,在临时文件夹里还原成图片文件,让图像控件去显示这个临时文件。

顺带说一句,Stream 对象要 ADO 2.5 以上才有。这个不用操心,现在还能跑起来的 Access 环境都远高于这个版本。

思路就这么点,下面动手。

三、建表

新建一张表,叫 tblPhotoDemo

  • PhotoID:自动编号,主键

  • Title:短文本,图片对应的标题,比如产品名称、员工姓名

  • PicName:短文本,保存图片原始文件名

  • PicExt:短文本,图片扩展名,比如 jpg、png

  • PicData:OLE 对象,用来保存图片二进制数据

  • UpdatedAt:日期/时间,记录上传或修改时间

看到 PicData 是 OLE 对象类型,可能有人要问:绕了半天,最后不还是 OLE 字段吗?

字段类型确实是,但这个类型说白了就是个能装二进制的容器,关键看你往里装什么。右键"插入对象"装进去的是带包装的 OLE 数据;我们用代码装进去的是图片文件的原始字节。容器一样,内容物完全不同。

四、建窗体

基于 tblPhotoDemo 建一个窗体,叫 frmPhotoDemo,放这几个控件:

  • txtTitle:文本框,绑定到 Title 字段,用来输入标题

  • txtPicName:文本框,绑定到 PicName 字段,显示图片文件名

  • imgPic:图像控件,不绑定字段,用来预览图片

  • cmdUpload:按钮,标题写"上传图片"或者"更换图片"

  • cmdDelete:按钮,标题写"删除图片"

两个容易踩的细节:

一是 imgPic 用的是图像控件,不是绑定对象框,别拖错了。它的"图片类型"属性要设成"链接",因为后面代码会把库里的二进制还原到临时文件,再让它显示这个临时文件。

二是 txtPicName 的"锁定"属性设成"是"。文件名由代码写入就行,让用户手填只会填出各种花样。


1.png

五、写代码

打开 frmPhotoDemo 的窗体代码模块,把下面这段整个贴进去。我用的是后期绑定写法,不用去引用里勾 ADO,贴完直接就能跑。

注意这里不要写成 Me!PicData = stm.Read。图片稍微大一点,就可能遇到“字段太小而不能接受所要添加的数据的数量”。OLE 对象字段这种长二进制数据,用 AppendChunk 写入会稳很多。

如果你已经遇到这个报错,先检查表设计:PicData 必须是“短文本/长文本/附件”以外的字段类型,这里要用 OLE 对象 字段来承接二进制数据。

Option Compare Database
Option Explicit

Private Const adTypeBinary As Long = 1
Private Const adModeReadWrite As Long = 3
Private Const adSaveCreateOverWrite As Long = 2
Private Const CHUNK_SIZE As Long = 32768

Private Sub cmdUpload_Click()
   On Error GoTo ErrHandler

   Dim filePath As String
   Dim stm As Object
   Dim rs As Object
   Dim binaryData As Variant

   filePath = PickImageFile()
   If Len(filePath) = 0 Then Exit Sub

   If Me.Dirty Then Me.Dirty = False

   Set stm = CreateObject("ADODB.Stream")
   stm.Type = adTypeBinary
   stm.Mode = adModeReadWrite
   stm.Open
   stm.LoadFromFile filePath
   stm.Position = 0
   binaryData = stm.Read

   Set rs = Me.RecordsetClone
   rs.Bookmark = Me.Bookmark

   rs.Edit
   rs!PicData = Null
   rs.Update

   rs.Bookmark = Me.Bookmark
   rs.Edit
   
   rs!PicName = GetFileNameOnly(filePath)
   rs!PicExt = GetFileExt(filePath)
   rs!UpdatedAt = Now()
   rs.Update

   Me.Refresh
   Me.imgPic.Picture = filePath
   MsgBox "图片上传成功。", vbInformation

CleanExit:
   On Error Resume Next
   If Not stm Is Nothing Then
       If stm.State <> 0 Then stm.Close
   End If
   If Not rs Is Nothing Then rs.Close
   Set stm = Nothing
   Set rs = Nothing
   Exit Sub

ErrHandler:
   MsgBox "图片上传失败:" & Err.Description, vbExclamation
   Resume CleanExit
End Sub

Private Sub cmdDelete_Click()
   On Error GoTo ErrHandler

   If IsNull(Me!PicData) Then
       Me.imgPic.Picture = ""
       Exit Sub
   End If

   If MsgBox("确定要删除当前记录中的图片吗?", vbQuestion + vbYesNo) <> vbYes Then Exit Sub

   Me!PicData = Null
   Me!PicName = Null
   Me!PicExt = Null
   Me!UpdatedAt = Now()
   Me.imgPic.Picture = ""

   If Me.Dirty Then Me.Dirty = False
   MsgBox "图片已删除。", vbInformation
   Exit Sub

ErrHandler:
   MsgBox "删除图片失败:" & Err.Description, vbExclamation
End Sub

Private Sub Form_Current()
   ShowCurrentPicture
End Sub

Private Sub ShowCurrentPicture()
   On Error GoTo ErrHandler

   Dim stm As Object
   Dim rs As Object
   Dim tempPath As String
   Dim totalSize As Long
   Dim offset As Long
   Dim bytesToRead As Long
   Dim chunkData As Variant

   If IsNull(Me!PicData) Then
       Me.imgPic.Picture = ""
       Exit Sub
   End If

   tempPath = BuildTempImagePath()

   Set rs = Me.RecordsetClone
   rs.Bookmark = Me.Bookmark

   If IsNull(rs!PicData.Value) Then
       Me.imgPic.Picture = ""
       GoTo CleanExit
   End If

   totalSize = rs!PicData.FieldSize
   If totalSize <= 0 Then
       Me.imgPic.Picture = ""
       GoTo CleanExit
   End If

   Set stm = CreateObject("ADODB.Stream")
   stm.Type = adTypeBinary
   stm.Mode = adModeReadWrite
   stm.Open

   Do While offset < totalSize
       bytesToRead = CHUNK_SIZE
       If offset + bytesToRead > totalSize Then bytesToRead = totalSize - offset

       chunkData = rs!PicData.GetChunk(offset, bytesToRead)
       stm.Write chunkData
       offset = offset + bytesToRead
   Loop

   stm.SaveToFile tempPath, adSaveCreateOverWrite
   stm.Close

   Me.imgPic.Picture = tempPath

CleanExit:
   On Error Resume Next
   If Not stm Is Nothing Then
       If stm.State <> 0 Then stm.Close
   End If
   If Not rs Is Nothing Then rs.Close
   Set stm = Nothing
   Set rs = Nothing
   Exit Sub

ErrHandler:
   Me.imgPic.Picture = ""
   Resume CleanExit
End Sub

Private Function PickImageFile() As String
   With Application.FileDialog(3)
       .Title = "请选择要上传的图片"
       .AllowMultiSelect = False
       .Filters.Clear
       .Filters.Add "图片文件", "*.jpg;*.jpeg;*.png;*.gif;*.bmp"

       If .Show = -1 Then
           PickImageFile = .SelectedItems(1)
       Else
           PickImageFile = ""
       End If
   End With
End Function

Private Function GetFileNameOnly(ByVal filePath As String) As String
   GetFileNameOnly = Mid$(filePath, InStrRev(filePath, "\") + 1)
End Function

Private Function GetFileExt(ByVal filePath As String) As String
   Dim fileName As String
   Dim dotPos As Long

   fileName = GetFileNameOnly(filePath)
   dotPos = InStrRev(fileName, ".")

   If dotPos > 0 Then
       GetFileExt = LCase$(Mid$(fileName, dotPos + 1))
   Else
       GetFileExt = "jpg"
   End If
End Function

Private Function BuildTempImagePath() As String
   Dim fileName As String
   Dim fileExt As String
   Dim recordKey As String

   fileName = Nz(Me!PicName, "preview.jpg")
   fileExt = Nz(Me!PicExt, "jpg")

   If Nz(Me!PhotoID, 0) > 0 Then
       recordKey = CStr(Me!PhotoID)
   Else
       recordKey = Format$(Now(), "yyyymmddhhnnss")
   End If

   If InStrRev(fileName, ".") = 0 Then
       fileName = fileName & "." & fileExt
   End If

   BuildTempImagePath = Environ$("Temp") & "\AccessPic_" & recordKey & "_" & fileName
End Function

六、代码逻辑说一下

注意,我这里用的绑定窗体,上传前先输入一下标题,上传后标题和图片文件名都会写进表里。

上传的时候,先弹文件对话框选图,选完用 ADODB.Stream 按二进制把文件读进来,再通过 RecordsetClone 找到当前记录,用 AppendChunk 写进 PicData 字段,顺手记一下文件名、扩展名和时间,最后让图像控件显示刚选的这张图。

切换记录的时候反过来:先看当前记录有没有 PicData,有就用 GetChunk 把二进制分块读出来,写回临时文件夹,还原成图片文件给 imgPic 显示;没有就清空预览。这部分逻辑在 Form_Current 事件里,所以翻记录的时候图片会自动跟着切。

整套东西拆开看就一句话:库里存的是图片原始字节,窗体上显示的是临时还原出来的文件。OLE 包装绕过去了,预览也没耽误。

2.png

七、一条记录多张图怎么办

上面的例子是一条记录一张图。要是需求变成"一条产品记录传多张图",很多人的第一反应是往表里加字段:

PicData1
PicData2
PicData3
PicData4

像这种横着加字段的设计,图片数量一变就要改表改窗体,迟早改到怀疑人生。正确的做法是拆成主表加明细表:

  • tblProduct:保存产品基本信息

  • tblProductPhoto:保存产品对应的多张图片

tblProductPhoto 可以这样设计:

  • PhotoID:自动编号,图片主键

  • ProductID:长整型,对应产品 ID

  • PicName:短文本,图片文件名

  • PicExt:短文本,图片扩展名

  • PicData:OLE 对象,图片二进制数据

  • SortNo:数字,图片排序

  • UpdatedAt:日期/时间,上传时间

窗体上用主窗体显示产品信息,子窗体显示图片列表,上传按钮放在子窗体里。一对多的关系,就该用一对多的表去装。

八、真要上项目,这几条限制加上

示例代码能跑,不代表能直接上生产。真放进项目里,建议把这几条也加上。

1. 限制图片大小

单张限制在 1MB 或 2MB 以内,上传前加一句判断就行:

If FileLen(filePath) > 2 * 1024 * 1024 Then
   MsgBox "图片不能超过 2 MB,请先压缩后再上传。", vbExclamation
   Exit Sub
End If

别嫌这条啰嗦。accdb 那 2GB 的上限就摆在那,不拦着点,用户什么都敢往里传——手机随手一拍就是十几 MB。

2. 统一图片格式

头像、产品图这类,统一成 JPG 或 PNG。格式越杂,后面导出、预览、迁移越闹心。

3. 记下谁传的、什么时候传的

表里加两个字段,UploadedBy 存上传人,UploadedAt 存上传时间。平时不起眼,出了问题要追溯的时候你会感谢自己。

4. 图真多的话,回到"文件夹 + 路径"

前面说过了,图片量大就别硬塞库里。文件放文件夹或服务器,库里只存路径、文件名、记录 ID、上传时间。库轻了,图片备份迁移也方便。

九、三种存法,最后摆在一起看

二进制存库里(本文的方法): 系统就一个文件,拷走就能用。代价是库会跟着图片长,只适合图少的场景。

图片存文件夹,库里存路径: 库轻,图片再多也不怕,但要管好文件夹和路径,换电脑、换服务器时要留神。

绑定对象框"插入对象": 操作最省事,体积代价最大。演示 demo 随便用,正式项目就算了。

我自己的取舍很简单:图少用第一种,图多用第二种,第三种基本不碰。

写在最后

Access里面是可以存图片的,但要注意存法。存错了,库就会膨胀得很快,影响性能和迁移。目前我这个方法也不最优解,最优解是把图片存到文件夹里,库里只存路径和文件名。这个我们下篇再讲。


如果你的团队正在用 Access,或者计划用 Access 搭建业务系统,我们可以提供从培训到落地的全流程支持:

技术培训

  • Access VBA 从入门到精通(线上/线下均可)

  • Access + SQL Server 企业级开发实战

  • Access 系统性能优化与架构设计

  • AI + Access 融合开发专题培训

定制开发

  • 企业 ERP / CRM / 进销存 / WMS / MES 等系统开发

  • 旧 Access 系统升级、性能优化与架构重构

  • AI 能力集成到现有 Access 业务系统

技术支持

  • 代码审查与重构建议

  • 疑难问题远程诊断

  • 一对一技术辅导

无论是想让团队快速上手 Access 开发,还是需要把现有系统接上 AI,都可以直接联系我们聊聊方案。

联系方式