
做 Excel 报表时,我最怕两件事:图表多,改起来全靠点鼠标;数据一换,图表范围、标题、标签又要手动改。VBA 图表的价值就在这里。它不是只会“画一张图”,它能把图表当成对象来管:创建、移动、改样式、换数据、批量导出、响应点击,还能做迷你图报表。
我这篇用自拟案例来讲。主线是一个连锁咖啡店看板,旁边穿插客服效果、SKU 出库、基金趋势这些小场景。案例和配图都重新做了,不沿用资料里的例子。
我先把图表对象认清楚
很多人写 VBA 图表卡住,不是语法问题,是对象没找对。

我会这样记:
- 嵌入式图表在普通工作表里,它有一个外壳,叫
ChartObject,也能从 Shapes 里访问。外壳管位置、大小、名称。 - 真正的图表本体叫
Chart。标题、系列、坐标轴、图例、绘图区,都在 Chart 下面。 - 图表工作表不是嵌在某个单元格区域里,而是工作簿里单独的一张图表页,入口是
Workbook.Charts。
我平时写代码会尽量写完整引用:
Sub RenameDashboardChart()Dim ws As WorksheetDim co As ChartObjectSet ws = ThisWorkbook.Worksheets("看板")Set co = ws.ChartObjects("chtStoreMonth") co.Chart.HasTitle = True co.Chart.ChartTitle.Text = "4 月门店销售"EndSub
这里的 co 是外壳,co.Chart 才是图表。说白了,外壳像相框,图表像相框里的画。
还有一个小坑我会刻意避开:ChartObject.Name 和 Chart.Name 可能不是同一个值。定位嵌入式图表时,我更喜欢用 ChartObject 的名字,比如 ws.ChartObjects("chtStoreMonth")。
我会用宏录制器,但不会照抄录制结果
宏录制器很有用。遇到不熟的属性,我会录一小段,看看 Excel 自己怎么写。可录制器常常生成这类代码:
ActiveSheet.ChartObjects("Chart 1").ActivateActiveChart.ChartTitle.Text = "4 月门店销售"
这段能跑,但批量处理时很容易出问题。我的习惯是录完就改成对象引用:
Dim ws As WorksheetDim co As ChartObjectSet ws = ThisWorkbook.Worksheets("看板")Set co = ws.ChartObjects("chtStoreMonth")co.Chart.ChartTitle.Text = "4 月门店销售"
Select、Activate、ActiveChart 不是不能用,只是它们依赖当前界面状态。代码越靠当前界面,越容易被用户点一下鼠标打断。
我创建图表时,会先分清两种放法

我一般这么选:看板里需要很多图,就用嵌入式图表;需要单独打印、单独展示的大图,就用图表工作表。
真正的图表可能有的人不知道,可以手动创建:

选中一个工作表标签页,右键,插入;

界面长这样:

嵌入式图表:放在工作表里
Excel 2013 之后,我常用 Shapes.AddChart2。它能直接指定样式、类型、位置和大小。

下面这个例子会在“看板”工作表里创建一张门店销售柱形图:
SubBuildCoffeeSalesChart()DimwsAsWorksheetDimshpAsShapeDimrngAsRangeSetws=ThisWorkbook.Worksheets("看板")Setrng=ws.Range("A1:F6")Setshp=ws.Shapes.AddChart2(_Style:=227,_XlChartType:=xlColumnClustered,_Left:=ws.Range("H2").Left,_Top:=ws.Range("H2").Top,_Width:=520,_Height:=300)shp.Name="chtStoreMonth"Withshp.Chart.SetSourceDataSource:=rng.HasTitle=True.ChartTitle.Text="4 月门店销售".SetElementmsoElementLegendBottomEndWithEndSub
如果要兼容旧版 Excel,我会改用 ChartObjects.Add:
Sub BuildChartForOldExcel()Dim ws As WorksheetDim co As ChartObjectSet ws = ThisWorkbook.Worksheets("看板")Set co = ws.ChartObjects.Add(Left:=360, Top:=40, Width:=520, Height:=300) co.Name = "chtStoreMonth"With co.Chart .ChartType = xlColumnClustered .SetSourceData Source:=ws.Range("A1:F6") .HasTitle = True .ChartTitle.Text = "4 月门店销售"EndWithEndSub

图表工作表:整张 Sheet 就是一张图
图表工作表的入口是 ThisWorkbook.Charts:
Sub BuildSalesChartSheet()Dim ws As WorksheetDim cht As ChartSet ws = ThisWorkbook.Worksheets("月度数据")Set cht = ThisWorkbook.Charts.AddWith cht .Name = "月销售大图" .ChartType = xlLineMarkers .SetSourceData Source:=ws.Range("A1:M4") .HasTitle = True .ChartTitle.Text = "门店月销售趋势"EndWithEndSub
我会把 Charts("月销售大图") 当成一张特殊的工作表来理解。它没有 ChartObject 外壳,自己就是 Chart。
我写图表代码时,会走固定 6 步

我的写法基本都是这 6 步:准备数据区域,放下图表外壳,绑定数据,确定图表类型,处理标题和样式,导出或加交互。
这套写法有个好处:从创建一张图,扩展到创建几十张图,只需要把“数据区域”和“图表位置”放进循环里。
我改图表样式,不会一张张点鼠标
图表样式常见的动作有这些:
- 改
ChartType,比如柱形图、折线图、散点图。 - 改
ChartStyle 和 ChartColor。不同版本编号会有差异,Excel 2013 之后常见样式编号更多。

我一般写一个小过程,传入任何 Chart 都能统一处理:
SubPolishChart(ByValchtAsChart,ByValtitleTextAsString)Withcht.HasTitle=True.ChartTitle.Text=titleText.SetElementmsoElementLegendBottom.SetElementmsoElementPrimaryValueGridLinesMajor.ChartStyle=201.ChartColor=13If.SeriesCollection.Count>0ThenWith.FullSeriesCollection(1).Format.Line.Weight=2.25.MarkerSize=6EndWithEndIfEndWithEndSub
FullSeriesCollection 我也会用到。它能处理完整系列集合,适合有筛选状态的图表。只是旧版 Excel 里不一定支持,我会按版本情况换回 SeriesCollection。
我会少用 ActiveChart,但也要懂它
ActiveChart 指当前激活的图表。如果当前没激活图表,它就是 Nothing。
Sub CheckActiveChart()If ActiveChart IsNothingThen MsgBox "当前没有激活图表"Else MsgBox "当前图表标题是:" & ActiveChart.ChartTitle.TextEndIfEndSub
激活嵌入式图表可以这样写:
Worksheets("看板").ChartObjects("chtStoreMonth").Activate
激活图表工作表可以这样写:
ThisWorkbook.Charts("月销售大图").Activate
取消激活没有专门的 Deactivate 方法。我会选中一个单元格:
Worksheets("看板").Range("A1").Select
还有一个细节:如果用户选中了图表里的标题、坐标轴、数据点,ActiveChart 依然可能返回父图表。所以我做“当前是否在图表里”的判断时,会把这个情况也算进去。
我移动、删除图表时,会重新接住对象引用
把嵌入式图表移到图表工作表,可以用 Location:
Sub MoveEmbeddedChartToChartSheet()Dim ws As WorksheetDim cht As ChartSet ws = ThisWorkbook.Worksheets("看板")Set cht = ws.ChartObjects("chtStoreMonth").Chart cht.Location Where:=xlLocationAsNewSheet, Name:="门店销售大图"EndSub
把图表工作表移回普通工作表也可以:
Sub MoveChartSheetBack() ThisWorkbook.Charts("门店销售大图").Location _Where:=xlLocationAsObject, Name:="看板"End Sub
我会在 Location 之后重新获取图表引用,因为图表所在位置变了,旧引用有时不再适合继续用。
删除嵌入式图表:
Worksheets("看板").ChartObjects("chtStoreMonth").Delete
删除图表工作表时,Excel 可能弹确认框,我会临时关掉提示,再马上打开:
SubDeleteChartSheetQuietly()Application.DisplayAlerts = FalseThisWorkbook.Charts("门店销售大图").DeleteApplication.DisplayAlerts = TrueEndSub
这类代码我会用错误处理包起来,避免中途报错后 DisplayAlerts 一直关着。
我批量处理图表,会同时扫嵌入式图表和图表工作表
很多工作簿里两种图表都有。只循环 ChartObjects,图表工作表会漏掉;只循环 Charts,嵌入式图表会漏掉。
Sub PolishAllChartsInWorkbook()Dim wb As WorkbookDim ws As WorksheetDim co As ChartObjectDim cht As ChartSet wb = ThisWorkbookForEach ws In wb.WorksheetsForEach co In ws.ChartObjects PolishChartco.Chart,ws.Name& " 图表"Next coNext wsForEach cht In wb.Charts PolishChart cht, cht.NameNext chtEndSub
调整大小和对齐时,我只改外壳属性:Left、Top、Width、Height。
Sub ArrangeChartsOnDashboard()Dim ws As WorksheetDim co As ChartObjectDim i AsLong, cols AsLongDimleft0AsDouble,top0AsDouble,wAsDouble,hAsDouble,gapAsDoubleSet ws = ThisWorkbook.Worksheets("看板") cols = 3 left0 = 30 top0 = 80 w = 320 h = 210 gap = 18ForEach co In ws.ChartObjects i = i + 1 co.Width = w co.Height = h co.Left=left0+((i-1)Modcols)*(w+ gap) co.Top=top0+Int((i-1)/cols)*(h+ gap)Next coEndSub
我用“小过程”批量创建很多小图
比如仓库运营里,我想看 12 个 SKU 最近 7 天出库趋势。手工做 12 张图太费劲,我会把“创建一张图”写成过程,再循环调用。

Sub BuildSkuTrendCharts()Dim ws As WorksheetDim r AsLong, lastRow AsLongDim leftPos AsDouble, topPos AsDoubleSet ws = ThisWorkbook.Worksheets("SKU趋势") lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).RowFor r = 2To lastRow leftPos=40+((r-2)Mod4)* 260 topPos=80+Int((r-2)/4)* 180 CreateSkuChart ws, r, leftPos, topPosNext rEndSubSub CreateSkuChart(ByVal ws As Worksheet, ByVal rowNum AsLong, _ByValleftPosAsDouble,ByValtopPosAsDouble)Dim co As ChartObjectDim rng As RangeSet rng = ws.Range(ws.Cells(rowNum, "B"), ws.Cells(rowNum, "H"))Set co = ws.ChartObjects.Add(leftPos, topPos, 230, 150)With co.Chart .ChartType = xlLineMarkers .SeriesCollection.NewSeries .SeriesCollection(1).Name = ws.Cells(rowNum, "A").Value .SeriesCollection(1).Values = rng .SeriesCollection(1).XValues = ws.Range("B1:H1") .HasTitle = True .ChartTitle.Text = ws.Cells(rowNum, "A").Value .Legend.DeleteEndWithEndSub
这段的关键不是 12 张图,而是“一个过程只管一张图”。以后要从 12 张变成 120 张,我只改循环范围。
我导出图表时,会优先用 Export
单张图表导出,Chart.Export 最直接。常见格式有 PNG、JPG、GIF。
Sub ExportOneChart()Dim ws As WorksheetDim cht As ChartDim fileName AsStringSet ws = ThisWorkbook.Worksheets("看板")Set cht = ws.ChartObjects("chtStoreMonth").Chart fileName = ThisWorkbook.Path & "\门店销售.png" cht.Export Filename:=fileName, FilterName:="PNG"EndSub
我做公众号配图或发邮件附件时,经常用这个办法。要批量导出,就把它放进循环。
Sub ExportAllEmbeddedCharts()Dim ws As WorksheetDim co As ChartObjectDim folder AsString folder = ThisWorkbook.Path & "\chart_export\"If Dir(folder, vbDirectory) = ""Then MkDir folderForEach ws In ThisWorkbook.WorksheetsForEach co In ws.ChartObjects co.Chart.Exportfolder&ws.Name&"_"&co.Name&".png", "PNG"Next coNext wsEndSub
资料里也提到过一种老办法:把工作簿另存为 HTML,然后到生成的文件夹里找图表图片。我现在更常用 Export,因为路径和文件名都好控制。
我改图表数据,分三种情况处理
图表数据不是只能靠 SetSourceData。我会按场景选方法。
整张图换范围:SetSourceData
cht.SetSourceData Source:=ws.Range("A1:F6")
这适合普通柱形图、折线图,数据结构比较规整。
单条系列换数据:SeriesCollection
With cht.SeriesCollection(1) .Name = ws.Range("B1") .XValues = ws.Range("A2:A13") .Values = ws.Range("B2:B13")EndWith
如果要新加一条系列:
With cht.SeriesCollection.NewSeries .Name = ws.Range("C1") .XValues = ws.Range("A2:A13") .Values = ws.Range("C2:C13")EndWith
直接看 SERIES 公式
每条系列背后都有一个 SERIES 公式,常见形式像这样:
=SERIES(系列名, 分类标签, 数值, 顺序)
气泡图多一个参数:
=SERIES(系列名, X值, Y值, 顺序, 气泡大小)
我想确认一张图到底用了哪些单元格时,会打印公式:
SubPrintSeriesFormula()DimsAsSeriesForEachsInActiveChart.SeriesCollectionDebug.Prints.FormulaNextsEndSub
要从公式里解析单元格区域,事情会比想象中麻烦。工作表名可能带空格,公式里可能有数组常量,地区设置还可能影响分隔符。所以我会先用 .Name、.XValues、.Values 这些属性处理常规需求,只有做图表审计工具时才去解析 SERIES 公式。
我会用选中单元格来驱动图表换数据
我很喜欢这种小交互:左边是指标清单,点“堂食订单”“外卖订单”“会员订单”,右边图表马上切换。
PrivateSub Worksheet_SelectionChange(ByVal Target As Range)Dim cht As ChartIf Intersect(Target, Me.Range("A2:A8")) IsNothingThenExitSubIf Target.CountLarge > 1ThenExitSubSet cht = Me.ChartObjects("chtTrend").ChartWith cht.SeriesCollection(1) .Name = Target.Value .Values = Me.Range(Target.Offset(0, 1), Target.Offset(0, 6)) .XValues = Me.Range("B1:G1")EndWith cht.ChartTitle.Text = Target.Value & " - 近 6 月趋势"EndSub
这里我没有重建图表,只换系列数据。体验会很顺,代码也短。
我给散点图加自定义标签,直接从单元格读文本
Excel 2013 之后,界面里可以选择“单元格中的值”做数据标签。VBA 里我常用更直接的办法:逐个点写 DataLabel.Text。

Sub ApplyPointLabelsFromCells()Dim cht As ChartDim labelRng As RangeDim i AsLongSet cht = Worksheets("改版效果").ChartObjects("chtBeforeAfter").ChartSet labelRng = Worksheets("改版效果").Range("A2:A9")With cht.SeriesCollection(1) .ApplyDataLabelsFor i = 1To .Points.Count .Points(i).DataLabel.Text = labelRng.Cells(i, 1).TextNext iEndWithEndSub
这个场景我会用在散点图上,比如“改版前后转化金额”。只有点,没有标签,读者很难知道哪个点对应哪个页面。标签来自单元格,维护起来最省心。
我在用户窗体里显示图表,会先导出成图片
UserForm 里不能直接塞一个工作表图表。我常用的办法是:把图表导出成 GIF 或 PNG,再加载到 Image 控件。
Sub ShowChartPreview() frmChartPreview.ShowEnd Sub
在 UserForm 里写:
PrivateSub UserForm_Initialize()Dim tmpFile AsStringDim cht As Chart tmpFile = Environ$("TEMP") & "\chart_preview.gif"Set cht = ThisWorkbook.Worksheets("看板") _ .ChartObjects("chtStoreMonth").Chart cht.Export Filename:=tmpFile, FilterName:="GIF"Me.Image1.Picture = LoadPicture(tmpFile)EndSub
我会把这招用在小工具里,比如弹出一个“图表预览”窗口,让用户确认后再导出。
我理解图表事件后,报表就能“点图说话”
图表事件挺有意思。常见事件有这些:
| |
|---|
Activate | |
Select | |
MouseDown | |
BeforeDoubleClick | |
Resize | |
Calculate | |
SeriesChange | |

图表工作表的事件最简单,直接在图表页代码窗口写:
PrivateSub Chart_MouseDown(ByVal Button AsLong, _ByValShiftAsLong, _ByValxAsLong, _ByValyAsLong)Dim elementID AsLong, arg1 AsLong, arg2 AsLongMe.GetChartElement x, y, elementID, arg1, arg2If elementID = xlSeries Then MsgBox "你点了第 " & arg2 & " 个数据点"EndIfEndSub
嵌入式图表麻烦一点,要用类模块。
类模块 CChartEvent:
PublicWithEvents C As ChartPrivateSub C_MouseDown(ByVal Button AsLong, _ByValShiftAsLong, _ByValxAsLong, _ByValyAsLong)Dim elementID AsLong, arg1 AsLong, arg2 AsLong C.GetChartElement x, y, elementID, arg1, arg2If elementID = xlSeries Then Sheets("华东").ActivateEndIfEndSub
普通模块里要把事件对象存成模块级变量,不然过程结束后事件会失效:
Dim gChartEvent As CChartEventSub HookChartEvent()Set gChartEvent = New CChartEventSet gChartEvent.C = Worksheets("看板").ChartObjects("chtRegion").ChartEndSub
我会把这招用在区域看板里:点“华东”柱子,就跳到华东明细;点“华南”柱子,就跳到华南明细。用户不需要再找按钮。
我还会记住 4 个图表小技巧
每张嵌入式图表单独打印
Sub PrintEachChart()Dim co As ChartObjectForEach co In Worksheets("看板").ChartObjects co.Chart.PrintOutNext coEndSub
这个适合一张工作表里有很多图,但老板想每张图单独一页。
做断开数据的图表
有时我需要发一份报告,不希望收件人改源数据后图表跟着变。我有两种做法。
一种是复制成图片:
ActiveChart.CopyPicture Appearance:=xlScreen, Format:=xlPictureWorksheets("报告").Paste
另一种是把系列的值写成数组,让图表不再指向单元格。这个写法要按图表类型小心处理,我通常只在固定模板里用。
SubFreezeSeriesValues()DimsAsSeriesForEachsInActiveChart.SeriesCollections.Values=s.Valuess.XValues=s.XValuesNextsEndSub
MouseOver 显示说明文字
鼠标悬停到某个柱子上,我会用 GetChartElement 找出系列和点,再显示一个文本框。文本内容放在工作表单元格里,比如“2 月外卖订单上涨,因为新开了夜间配送”。
这类提示比默认 ChartTip 灵活。我通常会在 Activate 事件里关掉默认提示:
Application.ShowChartTipNames = FalseApplication.ShowChartTipValues = False
离开图表时再打开。
滚动图表
长序列数据不一定要一次全画出来。我的做法是用命名区域控制可见窗口,比如 StartDay、NumDays、Increment,再用按钮或宏改变 StartDay。

Sub MoveChartWindow()With Worksheets("日数据") .Range("StartDay").Value = .Range("StartDay").Value + .Range("Increment").ValueEndWithEnd Sub
系列公式引用动态命名区域后,图表会跟着滚动。这个办法适合订单量、库存量、价格曲线这类长时间序列。
我把迷你图当成“单元格里的小图表”
迷你图是 Excel 2010 之后的功能,适合把一行数据的趋势放到末尾一列。

创建一组折线迷你图:
Sub AddFundSparklines() Dim ws As WorksheetSet ws = ThisWorkbook.Worksheets("基金趋势") ws.Range("H2:H9").SparklineGroups.Add _Type:=xlSparkLine, _ SourceData:="B2:G9"End Sub
常见类型有三种:
xlSparkColumnStacked100:赢输迷你图。
遍历迷你图组时,我会用下标:
Sub ReportSparklineGroups()Dim ws As WorksheetDim i AsLongSet ws = ThisWorkbook.Worksheets("基金趋势")For i = 1To ws.SparklineGroups.CountWith ws.SparklineGroups(i) Debug.Print .Location.Address, .SourceData, .CountEndWithNext iEndSub
我不会把迷你图当普通 Chart 来管。它有自己的对象:SparklineGroups 和 Sparkline。
我觉得最容易踩的坑,就这几类
| |
|---|
ActiveChart | |
| 嵌入式走 Worksheet.ChartObjects,图表页走 Workbook.Charts |
| |
ChartObject.Name | |
Location | |
| |
| 常规需求先用 .Name、.XValues、.Values |
| |
| |
VBA 图表这事,说到底就是把图表当对象来管。创建时先定位置和数据,维护时拿住 Chart,批量时循环 ChartObjects 和 Charts,交互时用事件,输出时用 Export。我掌握这些之后,做 Excel 看板就不用一张张图慢慢点了。