公司动态

WinForm‑DataGridView 转置表格实战

📅 2026/8/27 9:53:47
WinForm‑DataGridView 转置表格实战
WinForm DataGridView 转置监控表格实战一行参数、一列设备工业上位机本文为实战笔记模拟皮带秤监控场景。开发环境Visual Studio 2022.NET Framework 4.8C# WinForm运行结果显示1 业务背景与需求工业上位机监控场景常规表格是一行一条设备数据但本场景需要做转置布局表格行代表参数项运行状态、瞬时流量、一班产量班产量下面增加运载时间 | 运行时间表格列代表每一台设备1# 皮带秤、2# 皮带秤、3# 皮带秤…2 整体设计思路实现转置布局DataGridView监控表格一行代表一个监控参数一列代表一台皮带秤设备。设备台数固定为4台Model硬编码Value1~Value4属性。Model层 CommonParamRowModel实现INotifyPropertyChanged属性变更通知接口修改对象属性自动通知控件刷新界面。ParamName对应表格第一列存储参数名称Value1 ~ Value4分别对应4台皮带秤单元格数据赋值触发界面更新。优点逻辑简单缺点新增设备时需要手动增加Value属性。数据源绑定层使用BindingListCommonParamRowModel作为绑定数据源对接DataGridView属性变更实现局部单元格刷新解决表格刷新闪烁。关闭AutoGenerateColumns自动列所有列完全手动创建。动态创建表格列创建第一列参数名称绑定ParamName循环创建设备列DataPropertyName绑定Value1~Value4设备列使用Fill模式窗体大小变化自动平分宽度。初始化表格行数据遍历参数集合实例化Model对象加入BindingList数据源开启单元格WrapMode换行支持单元格内部\r\n多行文本设置表格只读、隐藏行表头开启双缓冲降低闪烁。UI样式封装 SetBaseDGV只处理外观尺寸、字体、配色、选中样式不处理业务数据。控件铺满窗体客户区手动控制行高表头高度关闭系统表头视觉样式自定义深蓝工控深色主题取消原生蓝色选中高亮全部文字居中关闭表头排序防止现场误操作。跨线程数据更新System.Timers.Timer模拟设备轮询通过SynchronizationContext切回UI线程。规则后台线程不直接操作DataGridView控件仅修改Model属性依靠INotifyPropertyChanged自动刷新界面。单元格状态着色 CellFormatting只做界面视觉渲染不改动原始业务数据。针对运行状态行根据状态文本设置不同警示字体颜色。资源释放窗体关闭时Stop、Dispose定时器释放后台任务避免内存泄漏。关键约束样式、行高相关操作必须等待列、行初始化完成之后执行当前Demo为固定设备台数版本动态可变设备需要改造Model。3 核心 Model 代码 CommonParamRowModelusingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Linq;usingSystem.Runtime.CompilerServices;usingSystem.Text;usingSystem.Threading.Tasks;namespaceDGV{publicclassCommonParamRowModel:INotifyPropertyChanged{privatestring_paramName;/// summary参数名称表格第一列/summarypublicstringParamName{get_paramName;set{_paramNamevalue;OnPropertyChanged();}}/// summary/// 存储第1台设备在当前参数行的值/// value显示值数字 / 字符串/// /summaryprivateobject_v1;publicobjectValue1{get_v1;set{_v1value;OnPropertyChanged();}}/// summary/// 存储第2台设备在当前参数行的值/// value显示值数字 / 字符串/// /summaryprivateobject_v2;publicobjectValue2{get_v2;set{_v2value;OnPropertyChanged();}}/// summary/// 存储第3台设备在当前参数行的值/// value显示值数字 / 字符串/// /summaryprivateobject_v3;publicobjectValue3{get_v3;set{_v3value;OnPropertyChanged();}}/// summary/// 存储第4台设备在当前参数行的值/// value显示值数字 / 字符串/// /summaryprivateobject_v4;publicobjectValue4{get_v4;set{_v4value;OnPropertyChanged();}}#regionINotifyPropertyChanged 固定模板publiceventPropertyChangedEventHandlerPropertyChanged;protectedvoidOnPropertyChanged([CallerMemberName]stringpropertyNamenull){PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(propertyName));}#endregion}}4 窗体初始化流程4.1 窗体成员变量// 1、绑定数据源存放一行行CommonParamRowModelprivateBindingListCommonParamRowModel_tableSource;// 2、UI线程上下文后台定时器切UI线程用privateSynchronizationContext_uiSyncContext;// 3、模拟设备列表代表有几台皮带秤privatereadonlystring[]_deviceNames{1#皮带秤,2#皮带秤,3#皮带秤,4#皮带秤};// 4、模拟参数列表表格每一行就是一个参数ListstringparamItemListnewListstring(){运行状态,瞬时流量(t/h),一班产量(t)\r\n\r\n运载(min)|运行(min),};// 5、模拟轮询定时器privateSystem.Timers.Timer_demoTimer;//6、随机数模拟数据RandomrandomnewRandom();4.2 FormLoad 初始化步骤privatevoidForm1_Load(objectsender,EventArgse){//窗体大小this.Width900;this.Height300;// 获取当前UI线程上下文给后面定时器用_uiSyncContextSynchronizationContext.Current;// 实例化绑定集合_tableSourcenewBindingListCommonParamRowModel();InitParamColData();InitParamRowData();dataGridView1.DataSource_tableSource;//列、行、绑定全部完成之后再调用样式SetBaseDGV();dataGridView1.CellFormattingdataGridView1_CellFormatting;//实例化定时器1000ms执行一次_demoTimernewSystem.Timers.Timer(1000);_demoTimer.ElapsedDemoTimer_Elapsed;_demoTimer.Start();}/// summary/// 初始化列/// /summaryprivatevoidInitParamColData(){//基础设置禁止自动生成列全部手动建列dataGridView1.AutoGenerateColumnsfalse;dataGridView1.Columns.Clear();//第一列参数名称DataGridViewTextBoxColumncolParamnewDataGridViewTextBoxColumn();colParam.NamecolParamName;//列的内部名字程序后台标识用界面看不见colParam.HeaderText设备名;//表头显示文字界面上看到的表头colParam.DataPropertyNameParamName;//【绑定核心】此列读取Model的 ParamName 属性colParam.Width300;//列像素宽度300colParam.ResizableDataGridViewTriState.False;//禁止鼠标拖拽修改这一列宽度dataGridView1.Columns.Add(colParam);//把这一列加到DGV控件//设备列指定 DataPropertyName和Model属性对应string[]propNames{Value1,Value2,Value3,Value4};for(inti0;i_deviceNames.Length;i){DataGridViewTextBoxColumndevColnewDataGridViewTextBoxColumn();devCol.HeaderText_deviceNames[i];//表头文字1#皮带秤、2#皮带秤...devCol.DataPropertyNamepropNames[i];//绑定Model对应属性第0列→Value1第1→Value2...dataGridView1.Columns.Add(devCol);}}/// summary/// 初始化行/// /summaryprivatevoidInitParamRowData(){_tableSource.Clear();foreach(stringpNameinparamItemList){CommonParamRowModelrownewCommonParamRowModel();row.ParamNamepName;//给【参数名称】赋值运行状态、瞬时流量、一班产量……row.Value1--;//1#秤初始显示--row.Value2--;//2#秤初始显示--row.Value3--;//3#秤初始显示--row.Value4--;//4#秤初始显示--_tableSource.Add(row);//把这一行model添加到绑定集合}//工业样式优化dataGridView1.ReadOnlytrue;dataGridView1.AllowUserToAddRowsfalse;dataGridView1.RowHeadersVisiblefalse;//开启双缓冲防闪烁(必须窗体加载完成之后再调用不要放在构造函数)SetDataGridViewDoubleBuffered(dataGridView1);}/// summary/// 外观样式函数只负责尺寸、颜色、字体、对齐、禁止拖拽、取消选中高亮、列填充不创建列、不填充业务数据。必须在列、行已经生成完成之后再调用。/// /summaryprivatevoidSetBaseDGV(){//1、控件整体位置大小跟随窗体尺寸适配//DGV宽度等于窗体内部可用客户区域铺满窗体横向this.dataGridView1.Widththis.ClientSize.Width;//DGV高度度等于窗体内部可用客户区域铺满窗体纵向this.dataGridView1.Heightthis.ClientSize.Height;//对齐左边this.dataGridView1.Left0;//对齐顶部this.dataGridView1.Top0;//单元格内容字体宋体18号加粗工控大屏看得清楚dataGridView1.FontnewFont(宋体,14,FontStyle.Bold);//表头单独字体微软雅黑20加粗表头比内容更大dataGridView1.ColumnHeadersDefaultCellStyle.FontnewFont(微软雅黑,16,FontStyle.Bold);//2、行高相关设置//关闭自动行高不根据文字内容自动撑开行高全部手动控制高度dataGridView1.AutoSizeRowsModeDataGridViewAutoSizeRowsMode.None;// 全局行设置固定行高禁止调整行高禁止用户用鼠标拖动修改行的高度dataGridView1.AllowUserToResizeRowsfalse;//表头高度禁止鼠标拖拽改变dataGridView1.ColumnHeadersHeightSizeModeDataGridViewColumnHeadersHeightSizeMode.DisableResizing;//把表格可用总高度平均分成9份得到每一行的目标高度 tmpHeightinttmpHeight(dataGridView1.Height)/4;dataGridView1.ColumnHeadersHeighttmpHeight;//表头高度使用这个计算值//循环每一行给每一行数据行设置行高//⚠️注意必须Rows已经存在才会生效如果还没有生成行这里循环等于什么都没做for(inti0;idataGridView1.Rows.Count;i){dataGridView1.Rows[i].HeighttmpHeight;}//3、列宽度第一列固定其余设备列自动平分剩余空间//i从1开始跳过第0列参数项目列它是固定宽度300//Fill模式所有这些列会平分DGV剩下的全部宽度窗体拉大列自动变宽for(inti1;idataGridView1.Columns.Count;i){dataGridView1.Columns[i].AutoSizeModeDataGridViewAutoSizeColumnMode.Fill;}//4、选中效果取消蓝色选中高亮工控界面常用dataGridView1.SelectionModeDataGridViewSelectionMode.CellSelect;//按单元格选择//选中时背景色 正常背景色肉眼看不到蓝色选中块逻辑仍然可以选中单元格只是视觉无变化dataGridView1.DefaultCellStyle.SelectionBackColordataGridView1.DefaultCellStyle.BackColor;dataGridView1.DefaultCellStyle.SelectionForeColordataGridView1.DefaultCellStyle.ForeColor;//5、单元格文字换行、内边距//开启单元格文字自动换行如果单元格文字有\r\n会多行展示dataGridView1.DefaultCellStyle.WrapModeDataGridViewTriState.True;//单元格内边距文字距离边框留出空隙不会贴边挤在一起dataGridView1.DefaultCellStyle.PaddingnewPadding(3,2,3,2);//表头单元格内边距dataGridView1.ColumnHeadersDefaultCellStyle.PaddingnewPadding(3,3,3,3);//6、文字居中//所有表头文字居中dataGridView1.ColumnHeadersDefaultCellStyle.AlignmentDataGridViewContentAlignment.MiddleCenter;//所有数据单元格文字居中dataGridView1.DefaultCellStyle.AlignmentDataGridViewContentAlignment.MiddleCenter;//7、关闭每一列点击表头排序功能//工控监控表一般不需要点击表头排序关闭避免用户误点界面乱掉for(inti0;ithis.dataGridView1.Columns.Count;i){this.dataGridView1.Columns[i].SortModeDataGridViewColumnSortMode.NotSortable;}//8、颜色主题深蓝工控深色风格//加上这句关闭系统主题对表头的覆盖自定义表头颜色才会生效dataGridView1.EnableHeadersVisualStylesfalse;//表头背景深蓝色dataGridView1.ColumnHeadersDefaultCellStyle.BackColorColor.FromArgb(20,45,90);//表头文字青蓝色dataGridView1.ColumnHeadersDefaultCellStyle.ForeColorColor.FromArgb(80,220,255);//表头选中背景和普通背景一致消除表头选中变色dataGridView1.ColumnHeadersDefaultCellStyle.SelectionBackColorColor.FromArgb(20,45,90);//数据行默认背景深蓝dataGridView1.DefaultCellStyle.BackColorColor.FromArgb(15,40,75);//数据行文字浅青白色dataGridView1.DefaultCellStyle.ForeColorColor.FromArgb(210,240,255);//点击单元格选中的背景色dataGridView1.DefaultCellStyle.SelectionBackColorColor.FromArgb(30,70,120);dataGridView1.DefaultCellStyle.SelectionForeColorColor.White;//网格分割线颜色淡青蓝色dataGridView1.GridColorColor.FromArgb(40,140,190);//9、单独修改第0列【参数项目列】样式和其他数据行区分开dataGridView1.Columns[0].DefaultCellStyle.BackColorColor.FromArgb(30,60,100);dataGridView1.Columns[0].DefaultCellStyle.ForeColorColor.FromArgb(80,220,255);}privatevoidSetDataGridViewDoubleBuffered(DataGridViewdgv){TypedgvTypedgv.GetType();PropertyInfopidgvType.GetProperty(DoubleBuffered,BindingFlags.Instance|BindingFlags.NonPublic);pi.SetValue(dgv,true,null);}重点列的DataPropertyName必须和 Model 属性名完全一致绑定才生效。5 CellFormatting 单元运行格状态着色只针对运行状态行做字体颜色其他行跳过跳过第一列参数名称。privatevoiddataGridView1_CellFormatting(objectsender,DataGridViewCellFormattingEventArgse){//只处理【运行状态行行号0】其他行直接跳过if(e.RowIndex!0)return;//跳过第一列参数名称“运行状态”那一格不改颜色if(e.ColumnIndex0)return;stringcellTexte.Value?.ToString()??;switch(cellText){case负料运行:case空料运行://e.CellStyle.ForeColor Color.Green;e.CellStyle.ForeColorColor.FromArgb(70,255,110);break;case设备停止:e.CellStyle.ForeColorColor.FromArgb(255,85,85);break;case通讯故障://e.CellStyle.BackColor Color.LightYellow;e.CellStyle.ForeColorColor.FromArgb(255,230,60);break;default:e.CellStyle.ForeColorColor.White;break;}}6 后台定时器调用示例Timer 回调属于非 UI 线程必须通过_uiSyncContext.Post切回 UI 线程再修改 Model禁止直接操作 DGV 控件。privatevoidDemoTimer_Elapsed(objectsender,ElapsedEventArgse){//切回UI线程只修改Model不动DataGridView控件_uiSyncContext.Post(_{//第0行运行状态_tableSource[0].Value1负料运行;_tableSource[0].Value2空料运行;_tableSource[0].Value3设备停止;_tableSource[0].Value4通讯故障;//第1行瞬时流量_tableSource[1].Value1random.Next(0,250);_tableSource[1].Value2random.Next(0,120);_tableSource[1].Value3random.Next(0,100);_tableSource[1].Value4random.Next(0,200);doubletmp12;//第2行一班产量_tableSource[2].Value1double.IsNaN(123.4)?--:123.4\r\n\r\ntmp | tmp;_tableSource[2].Value2double.IsNaN(123.4)?--:123.4\r\n\r\ntmp | tmp;_tableSource[2].Value3double.IsNaN(123.4)?--:123.4\r\n\r\ntmp | tmp;_tableSource[2].Value4double.IsNaN(123.4)?--:123.4\r\n\r\ntmp | tmp;},null);}7 释放资源privatevoidForm1_FormClosing(objectsender,FormClosingEventArgse){//释放定时器资源_demoTimer?.Stop();_demoTimer?.Dispose();}8 开发踩坑记录表头颜色设置不生效Windows 系统主题会覆盖表头样式必须设置dataGridView1.EnableHeadersVisualStyles false;。DGV 尺寸赋值后控件超出窗体边界不要直接使用Form.Width / Form.Height窗体标题栏、边框会占用尺寸计算控件大小时使用this.ClientSize。表格刷新闪烁不要直接修改dataGridView.Rows[x].Cells[y].Value修改 Model 属性依靠INotifyPropertyChanged自动局部刷新开启双缓冲关闭行自动尺寸。跨线程异常Timer 后台线程不能直接操作 UI 控件使用SynchronizationContext.Post切回 UI 线程只修改 Model 对象。9 小结核心思路Model 驱动 UI后台只改 Model不碰控件依靠绑定 属性通知完成界面刷新转置表格本质行是参数 Model 对象列动态生成每一列绑定对象的不同属性遇到界面异常优先排查绑定、跨线程、样式开关不要直接操作单元格。