ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Winform DataGridView显示图片的三种实现与避坑指南

Winform DataGridView显示图片的三种实现与避坑指南 简介在Winform开发中DataGridView通常用于展示文本表格但很多业务场景如商品列表、用户头像、订单缩略图需要将图片同步显示。这份PDF教程面向.NET Winform开发者讲解如何基于DataGridView的图片列与CellFormatting事件实现从文件路径动态加载并呈现图片。内容先介绍创建DataGridViewImageColumn并设置DataPropertyName与ImageLayout再说明在CellFormatting事件中读取路径、调用GetImage方法通过Image.FromStream安全加载图片同时兼顾空值判断与异常处理让实现过程更健壮。资源为1个PDF文件体积仅27KB代码示例集中、便于快速查阅。已有420人学习下载适合需要为数据网格增加图片展示能力的初级至中级开发者参考。1. DataGridView显示图片先搞清楚它为什么没你想的那么简单在Winform项目里把图片显示进DataGridView几乎是每个做管理端或监控界面的开发者都会遇到的需求设备照片、人员头像、产品缩略图、温湿度监控系统的曲线快照都想直接在表格里列出来。可真动手你会发现这功能远比拖一个控件复杂。红叉、变形、内存暴涨、滚动卡顿每一条都够你折腾半天。winform项目案例里图片列做得好不好直接影响整个winform界面美化的效果。这篇笔记把DataGridView显示图片的三种主流实现、参数设置、数据库读取链路和常见故障完整梳理一遍。适合正在用Winform做管理系统、winform industrial control类监控界面、以及winform温度湿度监控系统等需要大量图片展示场景的朋友。读完你不仅能跑通代码还能提前避开那些让你加班到深夜的坑。2. DataGridView图片列的三种实现绑定、自绘与小图标方案2.1 DataGridViewImageColumn直接绑定最小可用代码DataGridView自带的DataGridViewImageColumn是入门最快的方案。它支持直接绑定DataTable里Image类型的列也可以手动给Cell的Value赋一个Image对象。适合图片数量不大几百张以内、且图片已经加载成Image的场景。// 先准备好数据DataTable里增加一个Image类型的列 DataTable dt new DataTable(); dt.Columns.Add(ProductName, typeof(string)); dt.Columns.Add(ProductImage, typeof(Image)); // 填充行数据这里演示直接加载本地文件 dt.Rows.Add(产品A, Image.FromFile(C:\images\a.jpg)); dt.Rows.Add(产品B, Image.FromFile(C:\images\b.jpg)); // 绑定数据源DataGridView会自动生成列 dataGridView1.DataSource dt; // 拿到自动生成的图片列调整显示参数 DataGridViewImageColumn imgCol dataGridView1.Columns[ProductImage] as DataGridViewImageColumn; imgCol.ImageLayout DataGridViewImageCellLayout.Zoom; imgCol.Width 100; imgCol.HeaderText 产品图片;关键参数是ImageLayout它直接决定图片在单元格里的观感。Zoom会等比例缩放并保持宽高比不会把产品图拉成长条形Stretch强行填满单元格适合背景图但会让照片变形NotSet按图片原始尺寸绘制大图会溢出单元格。对产品图、设备照片这类内容建议一律用Zoom配合列宽80到120、行高60到90是winform界面美化里最常见的参数组合。如果数据源里只有byte[]需要先转成Image。常见做法是遍历DataTable的每一行用MemoryStream包住字节数组再调用Image.FromStream。这里有个极其隐蔽的坑Image.FromStream返回的Image与流是延迟绑定关系图片真正绘制时才去流里读数据。你如果提前把MemoryStream释放掉绘制时就会抛ArgumentException。我在项目里的处理方式是读取后立刻生成缩略图并把原图Dispose掉让缩略图与旧流完全脱钩后文会详细讲。直接绑定的缺点非常明显每行都持有完整的Image对象。一张2000x1500的JPG解码后大约占8MB内存300行就是2.4GB程序不卡死才怪。所以这个方案只适合小数据量、或数据源本身就是小缩略图的场景。数据量一旦超过五百行就要考虑下面两种方案。2.2 CellPainting自绘大图和滚动卡顿的正解当数据量超过几百行、且单张图片较大时用自绘代替直接绑定是更可靠的做法。常见的做法是用一列DataGridViewTextBoxColumn存储图片路径绑定数据时只赋路径字符串不创建任何Image对象。然后在CellPainting事件里拿到路径按需读取图片并绘制。这样内存里只有当前可见行的图片不可见行不占任何资源。// 先用一个文本框列存储图片路径 dataGridView1.Columns.Add(Id, ID); dataGridView1.Columns.Add(ImagePath, 图片); // 添加行时只存路径不加载图片 dataGridView1.Rows.Add(001, C:\images\device_001.jpg); dataGridView1.Rows.Add(002, C:\images\device_002.jpg); // 在CellPainting事件里接管图片列的绘制 private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { if (e.RowIndex 0 || e.ColumnIndex ! 1) return; // 先绘制背景、边框、选中高亮等默认内容 e.Paint(e.CellBounds, DataGridViewPaintParts.All); string path dataGridView1.Rows[e.RowIndex].Cells[ImagePath].Value?.ToString(); if (string.IsNullOrEmpty(path) || !File.Exists(path)) { // 文件不存在时画一个占位文字 e.Graphics.DrawString(无图片, dataGridView1.Font, Brushes.Gray, e.CellBounds.X 8, e.CellBounds.Y e.CellBounds.Height / 2 - 8); e.Handled true; return; } using (Image img Image.FromFile(path)) { Rectangle dest CalculateZoomRect(img, e.CellBounds); e.Graphics.DrawImage(img, dest); } e.Handled true; } // 等比例缩放并居中 private Rectangle CalculateZoomRect(Image img, Rectangle cell) { float ratio Math.Min((float)cell.Width / img.Width, (float)cell.Height / img.Height); int w (int)(img.Width * ratio); int h (int)(img.Height * ratio); return new Rectangle( cell.X (cell.Width - w) / 2, cell.Y (cell.Height - h) / 2, w, h); }这段代码里最关键的是e.Handled true。CellPainting触发后DataGridView默认还会继续执行自己的绘制逻辑如果不在代码末尾标记Handled自己画的图片会被系统绘制覆盖掉。所以第一行先调用e.Paint(e.CellBounds, DataGridViewPaintParts.All)把背景、边框、选中高亮一次性画出来再在上面叠加图片这样选中行时高亮背景依然正常显示。CalculateZoomRect这个辅助方法做的是“等比例缩放加居中”。DrawImage的原生行为是把图片左上角放到指定坐标不处理比例。你先算出目标矩形再把矩形坐标传给DrawImage图片就会按比例缩放并居中不会拉伸。自绘方案的问题是每次滚动都会执行Image.FromFile磁盘IO比较频繁。优化做法是加一层内存缓存见第5章。如果图片文件特别大建议在入库时就生成200px左右的中间缩略图DataGridView直接读缩略图文件速度能快一个数量级。这也是winform项目案例里设备监控大列表的标配做法。2.3 ImageList DataGridViewImageColumn状态小图标的轻量方案还有一类需求不是展示照片而是在表格里显示在线状态、告警等级、运行模式这类小图标。图标固定为16x16或24x24数量不超过几十个用ImageList配合图片列是最省事的方案。// ImageList装载状态图标 ImageList iconList new ImageList(); iconList.ImageSize new Size(24, 24); iconList.ColorDepth ColorDepth.Depth32bpp; iconList.Images.Add(online, Properties.Resources.icon_online); iconList.Images.Add(offline, Properties.Resources.icon_offline); iconList.Images.Add(fault, Properties.Resources.icon_fault); // 图片列绑定ImageList DataGridViewImageColumn iconCol new DataGridViewImageColumn(); iconCol.ImageList iconList; iconCol.Width 40; dataGridView1.Columns.Add(iconCol); // 添加行时Value里放图片的Key不是Image对象 dataGridView1.Rows.Add(设备1, online); dataGridView1.Rows.Add(设备2, fault);注意当图片列设置了ImageListValue属性传的是图片Key或索引DataGridView内部会自动去ImageList里取图。很多人习惯性往Value里塞Image对象结果界面一片空白或红叉。这一点和直接绑定Image的方案要区隔开属于同一个控件两种完全不同的用法。图标方案在winform industrial control和winform温度湿度监控系统这类界面里特别常见。设备状态一列、告警等级一列后续要更新状态时只需要重新给对应单元格的Value赋一个新的Key字符串再调用InvalidateCell刷一下性能开销极小。三种方案怎么选我整理了一个快速对比方案适用场景内存占用灵活性实现成本ImageColumn直接绑定少于500张小图高低最低CellPainting自绘大图、上千行低高中ImageList小图标固定状态图标最低中低选型依据就一条图片内容是否固定。固定用ImageList小数据量用直接绑定数据量大且图片内容动态变化就自绘。这个顺序基本能覆盖所有真实项目。3. 从数据库读取图片到DataGridView二进制转换、缩略图与异步加载3.1 数据库二进制图片字段的读取与转换很多Winform项目把图片直接存数据库SQL Server一般是image或varbinary(max)字段SQLite是BLOB。读取后拿到的是字节数组必须先转换成Image才能被DataGridView使用。下面以SQL Server为例string sql SELECT Id, ProductName, ProductImage FROM Products WHERE CategoryId catId; DataTable dt new DataTable(); using (SqlConnection conn new SqlConnection(connectionString)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(catId, categoryId); conn.Open(); using (SqlDataAdapter da new SqlDataAdapter(cmd)) { da.Fill(dt); } } // 增加一个Image类型的列用于后续绑定显示 dt.Columns.Add(Thumb, typeof(Image)); foreach (DataRow row in dt.Rows) { if (row[ProductImage] ! DBNull.Value) { byte[] bytes (byte[])row[ProductImage]; using (MemoryStream ms new MemoryStream(bytes)) { Image original Image.FromStream(ms); // 生产环境不要直接绑原图生成缩略图后再用 row[Thumb] CreateThumbnail(original, 120, 90); original.Dispose(); // 原图用完立刻释放 } } else { // 空值给一个空白位图避免DataGridView显示红叉 row[Thumb] new Bitmap(120, 90); } } dataGridView1.DataSource dt;这段代码有两个容易翻车的细节。第一个是我刚才提到的延迟绑定问题Image.FromStream创建的Image只在使用时才真正读取流数据。上面这种写法里缩略图生成完成后缩略图是一个全新的Bitmap对象与旧流没有关系所以MemoryStream在using结束时释放是安全的。如果你在转换后把Image直接赋给DataGridView那就不能在这个位置释放流。第二个细节是DBNull值的处理。数据库里没图片时字段值是DBNull直接赋给Image类型的列DataGridView显示红叉且可能抛异常。我习惯在空值分支赋一个同尺寸的空白Bitmap界面干净也不崩。如果你不想额外分配空白位图也可以先把DBNull映射到null再在CellPainting里做兜底绘制。3.2 缩略图生成尺寸、插值算法与内存释放缩略图是图片列表的性能核心。所有项目里展示到DataGridView的都是缩略图而非原图原因很直接一张1920x1080的图片解码后占用约8MB内存缩到120x90后只有约43KB内存差距接近两百倍。缩略图的生成质量直接决定界面观感。public static Bitmap CreateThumbnail(Image src, int maxWidth, int maxHeight) { // 等比例计算缩略图尺寸长宽均不超过maxWidth和maxHeight float ratio Math.Min((float)maxWidth / src.Width, (float)maxHeight / src.Height); int newW Math.Max(1, (int)(src.Width * ratio)); int newH Math.Max(1, (int)(src.Height * ratio)); Bitmap bmp new Bitmap(newW, newH); using (Graphics g Graphics.FromImage(bmp)) { // 高质量插值避免缩略图边缘出现锯齿 g.InterpolationMode InterpolationMode.HighQualityBicubic; g.SmoothingMode SmoothingMode.HighQuality; g.PixelOffsetMode PixelOffsetMode.HighQuality; g.DrawImage(src, 0, 0, newW, newH); } return bmp; }InterpolationMode决定缩放质量。默认Default使用低质量插值图片缩小后边缘锯齿明显在界面上看很廉价。HighQualityBicubic是GDI里质量最高的插值模式适合照片类图片缩小。SmoothingMode和PixelOffsetMode主要影响矢量图形边缘对照片影响不大但设置上无副作用。生成缩略图后的内存管理同样重要。CreateThumbnail内部创建的Graphics对象通过using释放调用方负责释放原图。这里有一个经常出现的写法错误生成缩略图后没有Dispose原图导致内存里同时存在原图和缩略图两份对象。前面3.1的代码里已经演示了正确姿势生成缩略图后立刻执行original.Dispose()确保原图不残留。如果缩略图需要反复使用可以把生成的Bitmap缓存到内存字典里也可以直接保存成临时文件。保存文件的好处是下次直接从磁盘加载缩略图不需要再次读取原图并解码加载速度会更快。文件缓存的目录建议放在Path.GetTempPath()下程序退出时由系统自动清理。3.3 异步加载与分页让大数据量图片不卡死界面在UI线程一次性加载一千张图片就算全是缩略图界面也会白屏好几秒。这在winform温度湿度监控系统这类需要快速切换数据范围的界面里完全不可接受。解决方案是异步加载加分页。private async Task LoadImageRowsAsync(int pageIndex, int pageSize) { // 后台线程只负责读取数据库返回byte[]数组 var rows await Task.Run(() { string sql SELECT Id, ProductImage FROM Products ORDER BY Id $OFFSET {pageIndex * pageSize} ROWS FETCH NEXT {pageSize} ROWS ONLY; DataTable dt QueryData(sql); var result new Listobject[](); foreach (DataRow r in dt.Rows) { result.Add(new object[] { r[Id], r[ProductImage] }); } return result; }); // 回到UI线程后再生成缩略图并填充表格 dataGridView1.SuspendLayout(); dataGridView1.Rows.Clear(); foreach (object[] row in rows) { byte[] bytes row[1] as byte[]; if (bytes null) continue; using (var ms new MemoryStream(bytes)) using (var src Image.FromStream(ms)) { dataGridView1.Rows.Add( row[0].ToString(), CreateThumbnail(src, 100, 75)); } } dataGridView1.ResumeLayout(); }关键决策是数据库读取放后台线程但Image对象的创建和缩略图生成必须回UI线程。GDI对象是非托管资源跨线程传递偶尔能显示但会在随机位置抛“The object is currently in use elsewhere”异常。把byte[]传回UI线程在UI线程做解码和缩略图生成线程模型最安全不会出离线问题。SuspendLayout和ResumeLayout必须成对使用作用是暂停表格的重绘流程。如果不调用每执行一次Rows.AddDataGridView都会重新布局一次几百行数据会触发几百次布局计算填充时间翻倍。加了这两行后布局只做一次性能提升非常明显。分页本身也是控制内存的手段。每页只加载50到100行滚动到下一页再加载内存峰值可以控制在几十MB内。如果不想用分页可以一次性读取全部数据但把缩略图生成和图片绘制都放到懒加载流程里。两者组合起来两千行图片列表的滚动表现与普通文本表格几乎无差别。4. 显示图片时的避坑指南五个常见问题与排查4.1 图片列显示红叉或空白现象DataGridViewImageColumn里没有图片取而代之一个红色叉号图标或整列空白。原因红叉表示Value里不存在合法的Image对象。最常见的有三种情况赋了DBNull而不是Image对象赋了byte[]而不是ImageImage对象被提前Dispose界面绘制时资源已经被回收。还有一个隐蔽场景是把MemoryStream放在using里提前释放Image.FromStream创建的Image失去了数据源绘制时抛ArgumentException界面最终显示为红叉。解决绑定数据源前遍历DataTable把所有byte[]统一转换成ImageDBNull统一替换成空白位图。检查代码里有没有对Image提前调用Dispose。用MemoryStream时延长流的生命周期或者读取后立刻生成缩略图并切断原图引用。排查时直接在Value上打断点看对象类型是不是System.Drawing.Image这一条能确认八成问题。4.2 图片变形、拉伸或模糊现象正方形的产品图在单元格里被拉成长条或缩略图边缘发虚有锯齿。原因变形几乎都出在ImageLayout设置上Stretch会强行填满单元格导致比例失真。模糊则来自缩略图生成时的插值算法太差或者原图本身分辨率不够放大之后像素被抽稀。解决展示照片一律用Zoom布局保证比例不失真。缩略图生成时使用HighQualityBicubic插值不要用系统默认的低质量插值。如果原图本身就是32x32的小图标强行放大成100x90肯定会糊这种场景保留原大小居中显示或者让美工换更高分辨率的素材。另外Winform在高DPI显示器上要主动开启Application.SetHighDpiMode(HighDpiMode.SystemAware)否则GDI在150%缩放的系统里会进一步劣化图片清晰度。4.3 滚动卡顿、内存持续上涨现象数据量到几百行后上下滚动一次卡一下任务管理器里内存一路涨到几百MB继续操作后可能直接崩溃。原因每个单元格持有一个未被释放的Image对象滚动时DataGridView不断创建绘制缓存旧缓存没有被及时回收。图片如果还是大图内存涨幅会非常夸张。这和Windows照片查看器提示“无法显示此图片因为计算机上的可用内存可能不足”是同一类问题本质都是GDI非托管资源耗尽。解决切换到CellPainting自绘或缩略图方案让单元格只存路径或ID不持有Image对象。缩略图长边控制在100到150px之间单张内存占用降到50KB以内。生成缩略图时确保finally里释放Image、Graphics、MemoryStream。再检查一下是否在CellMouseEnter或RowEnter事件里重复加载图片有些代码进一次单元格读一次文件这种写法必卡。滚动事件里只做缓存查找不做磁盘读取。4.4 修改图片后界面不刷新现象代码里更新了某一行的图片数据但表格显示的还是旧图无论怎么点都不变。原因DataGridView不会自动感知外部对DataTable或单元格Value的修改。给Rows[i].Cells[...].Value赋了新Image对象后界面没有收到重绘通知。有时候赋值时用的还是同一个Image对象引用图片文件内容变了而对象没变也没有任何事件触发重绘。解决更新Value后显式刷新单元格或整表。dataGridView1.Rows[rowIndex].Cells[ProductImage].Value newImage; // 只重绘目标单元格开销最小 dataGridView1.InvalidateCell(dataGridView1.Rows[rowIndex].Cells[ProductImage]); // 或者整表刷新 dataGridView1.Refresh();如果是整列数据都变了直接给DataSource赋一个全新的DataTable最省事。只改一张图时InvalidateCell比Refresh高效得多。注意重新赋值前把旧Image对象Dispose掉否则每次更新都多一份残留在内存里。4.5 自绘模式下选中高亮和边框丢失现象用CellPainting画完图片后点击这一行原本应该出现的蓝色高亮背景不见了单元格之间的边框线也时有时无。原因设置了e.Handled true之后DataGridView的默认绘制流程被完全跳过。如果自绘代码里没有先调用e.Paint绘制默认内容背景、边框、选中高亮全部丢失画面就成了背景上直接贴一张图。解决自绘代码第一步先调用e.Paint(e.CellBounds, DataGridViewPaintParts.All)把所有默认内容画出来再在上面叠加自己的图片。如果只想部分接管可以用DataGridViewPaintParts.Background、Border、SelectionBackground这几个枚举组合让系统画背景和边框图片自己画。另外把SelectionMode设为FullRowSelect整行高亮比单格高亮在视觉上更完整也能掩盖部分绘制顺序问题。这五类问题覆盖了winform项目案例里绝大多数图片显示故障每一条都有明确的现象特征和排查方向。按这个顺序检查基本半小时内能定位问题。5. 进阶点击预览、缩略图缓存与高DPI适配前面解决了能显示最后解决好用。三个进阶点点击预览、缩略图缓存和高DPI适配。单击单元格弹出预览窗体是最常见的需求在CellClick里拿到图片路径弹一个带PictureBox的窗体显示原图。private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex 0 || e.ColumnIndex ! 1) return; string path dataGridView1.Rows[e.RowIndex].Cells[ImagePath].Value?.ToString(); if (string.IsNullOrEmpty(path) || !File.Exists(path)) return; using (Form preview new Form()) using (Image fullImage Image.FromFile(path)) { preview.Text Path.GetFileName(path); preview.StartPosition FormStartPosition.CenterParent; preview.Size new Size( Math.Min(fullImage.Width 40, 1200), Math.Min(fullImage.Height 40, 800)); PictureBox pb new PictureBox { Dock DockStyle.Fill, Image fullImage, SizeMode PictureBoxSizeMode.Zoom }; preview.Controls.Add(pb); preview.ShowDialog(this); } }这里using包裹了Form和Image关闭预览窗体后原图和窗体资源一并释放。大图的解码会造成短暂卡顿后续可以改成异步加载。如果图片是按ID从数据库读取的把bytes转Image的逻辑也放到Task.Run里。缩略图缓存建议用ConcurrentDictionary避免每次CellPainting都读磁盘。static ConcurrentDictionarystring, Bitmap thumbCache new ConcurrentDictionarystring, Bitmap(); private Bitmap GetThumb(string path) { if (thumbCache.TryGetValue(path, out var cached)) { return cached; } using (var src Image.FromFile(path)) { var thumb CreateThumbnail(src, 120, 90); thumbCache[path] thumb; return thumb; } }缓存容量必须控制超过200个就整体清理一次否则缓存本身会成为新的内存问题。另一个血泪教训是缓存的Bitmap只能在UI线程使用不要在后台线程预生成后跨线程写入否则绘制时随机崩。需要后台生成就先写临时文件再在UI线程从文件加载。高DPI适配很直接程序入口加Application.SetHighDpiMode(HighDpiMode.PerMonitorV2)主窗体设置AutoScaleMode.Dpi。否则在150%缩放的笔记本上图片列会比设计时模糊一圈整体界面质感下降明显。我自己的习惯是小项目先用2.1直接绑定堆量验证效果后再升级成自绘避免过度设计凡是数据量超过两百行的项目直接上CellPainting加缓存一步到位。这条路走完DataGridView显示图片基本不会再成为Winform开发的瓶颈。希望帮到你。本文还有配套的精品资源点击获取
返回列表