尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Winform上位机也能写出媲美Avalonia的界面

Winform上位机也能写出媲美Avalonia的界面 (动态效果图请前往微信公众号(上位机康工))工欲善其事必先利其器传统Winform上位机程序主要重心都是倾向于和业务系统通信和PLC下位机的交互上这个无论是站在项目进度上看还是在项目交付时间上都是没有问题的。但是从客户体验感的角度看相比WPF或者Avalonia开发的界面就过于捉襟见肘了.尤其是对于大型企业仓库摆满了各种各样的竞争对手设备参观的人络绎不绝各种效率精度体验对比之下路过咱们设备的时候看到东倒西歪的控件按钮是什么感觉O(∩_∩)O交互原理上图文是实际上位机和下位机通过原始TCP长连接通信交互上位机作为客户端分帧方式是按完整JSON object的大括号边界分帧不使用换行符或长度头。应答机制为当前无通用ACK/NACK。目标分配成功不应答指定拒绝场景返回异常类型。除了下发格口是由上位机发给下位机其余生成包裹包裹落格包裹分拣完成等等均由下位机服务端上发TCP允许拆包和粘包连续发送{...}{...}或把一个JSON对象拆成多次发送下位机均按照对象边界处理功能模块本文以原始TCP长连接方式和下位机通信运行流程如下——监听下位机包裹流水号生成——匹配相机解码——和客户WCS/WMS系统交互——本地格口映射——通知下位机分拣规则格口——下位机反馈包裹落格动作——下位机反馈包裹分拣完成 。以上是完整的运作流程。辅助功能模块包括分拣计划配置用于格口映射历史数据查询格口分布报表每小时分拣效率报表等等源码共享系统功能细节篇幅有限暂不做过多介绍本文主要分享上位机UI界面动画效果以及实现原理和源码分享。Label外边框标识实际项目浅绿色外边框标识控件表示已经分配了目标格口的包裹红色边框标识的控件表示尚未映射格口或者分拣到异常的包裹效果具体实现方法如下lbl.Paint (sender, e) { Label lb sender as Label; if (lb ! null lb.Tag is Color color) { ControlPaint.DrawBorder(e.Graphics, lb.ClientRectangle, color, 2, ButtonBorderStyle.Solid, color, 2, ButtonBorderStyle.Solid, color, 2, ButtonBorderStyle.Solid, color, 2, ButtonBorderStyle.Solid); } };如果需要动态规划label外边框颜色可以在生成label控件的时候将动态规划颜色存储到Tag属性中方便后续动态规划边框颜色特效DataGridView表格美化废话不多说直接上源码效果如顶部主图1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798#region 表格美化// 1. 启用双缓冲彻底解决绘制闪烁问题typeof(DataGridView).InvokeMember(DoubleBuffered,System.Reflection.BindingFlags.NonPublic |System.Reflection.BindingFlags.Instance |System.Reflection.BindingFlags.SetProperty,null, dataGridView1,newobject[] {true});// 2. 订阅核心事件dataGridView1.CellMouseEnter DataGridView1_CellMouseEnter;dataGridView1.CellMouseLeave DataGridView1_CellMouseLeave;dataGridView1.MouseLeave DataGridView1_MouseLeave;dataGridView1.RowPostPaint DataGridView1_RowPostPaint;dataGridView1.Scroll (s, e) {if(_hoverRowIndex 0) dataGridView1.Invalidate(); };// 3. 当数据源变动如刷新、排序、加载时重置边框到第一行dataGridView1.DataSourceChanged (s, e) { _hoverRowIndex 0; dataGridView1.Invalidate(); };dataGridView1.RowsAdded (s, e) {if(_hoverRowIndex -1) { _hoverRowIndex 0; dataGridView1.Invalidate(); } };// 初始化 DataGridView 数据源_gridTable newDataTable();_gridTable.Columns.Add(扫码时间,typeof(string));_gridTable.Columns.Add(条码,typeof(string));_gridTable.Columns.Add(包裹ID,typeof(long));_gridTable.Columns.Add(长度,typeof(decimal));_gridTable.Columns.Add(宽度,typeof(decimal));_gridTable.Columns.Add(高度,typeof(decimal));_gridTable.Columns.Add(重量,typeof(decimal));_gridTable.Columns.Add(目标格口,typeof(int));_gridTable.Columns.Add(实际格口,typeof(int));_gridTable.Columns.Add(状态,typeof(string));dataGridView1.DataSource _gridTable;dataGridView1.AutoSizeColumnsMode DataGridViewAutoSizeColumnsMode.None;// 设置各列的 FillWeight权重越大列越宽dataGridView1.Columns[扫码时间].FillWeight 1.8f;dataGridView1.Columns[条码].FillWeight 2.0f;// 加宽dataGridView1.Columns[包裹ID].FillWeight 1.8f;// 加宽dataGridView1.Columns[长度].FillWeight 0.6f;// 缩短dataGridView1.Columns[宽度].FillWeight 0.6f;// 缩短dataGridView1.Columns[高度].FillWeight 0.6f;// 缩短dataGridView1.Columns[重量].FillWeight 0.6f;// 缩短dataGridView1.Columns[目标格口].FillWeight 0.8f;dataGridView1.Columns[实际格口].FillWeight 0.8f;dataGridView1.Columns[状态].FillWeight 1f;// 美化dataGridView1.RowHeadersVisible false;// 去掉行头空白列dataGridView1.SelectionMode DataGridViewSelectionMode.FullRowSelect;dataGridView1.ReadOnly true;dataGridView1.AllowUserToAddRows false;dataGridView1.BackgroundColor Color.FromArgb(3, 11, 23);dataGridView1.ForeColor Color.FromArgb(126, 181, 199);dataGridView1.Font newFont(宋体, 9F);// 列头样式背景色 0,51,119字体不加粗字体颜色 CyandataGridView1.EnableHeadersVisualStyles false;// 必须dataGridView1.ColumnHeadersDefaultCellStyle.Font newFont(宋体, 10F, FontStyle.Regular);dataGridView1.ColumnHeadersDefaultCellStyle.BackColor Color.FromArgb(0, 51, 119);dataGridView1.ColumnHeadersDefaultCellStyle.ForeColor Color.Cyan;// 数据行样式dataGridView1.DefaultCellStyle.BackColor Color.FromArgb(3, 11, 23);dataGridView1.DefaultCellStyle.ForeColor Color.Cyan;dataGridView1.AlternatingRowsDefaultCellStyle.BackColor Color.FromArgb(20, 30, 50);dataGridView1.AutoSizeColumnsMode DataGridViewAutoSizeColumnsMode.Fill;dataGridView1.RowTemplate.Height 30;dataGridView1.ColumnHeadersDefaultCellStyle.Alignment DataGridViewContentAlignment.MiddleCenter;dataGridView1.DefaultCellStyle.Alignment DataGridViewContentAlignment.MiddleCenter;// 水平垂直居中dataGridView1.AlternatingRowsDefaultCellStyle.Alignment DataGridViewContentAlignment.MiddleCenter;// 列头高度与行高一致去除边框dataGridView1.ColumnHeadersHeight dataGridView1.RowTemplate.Height 20;dataGridView1.ColumnHeadersBorderStyle DataGridViewHeaderBorderStyle.None;dataGridView1.CellBorderStyle DataGridViewCellBorderStyle.None;// 完全无边框dataGridView1.AllowUserToOrderColumns false;foreach(DataGridViewColumn colindataGridView1.Columns){col.SortMode DataGridViewColumnSortMode.NotSortable;}dataGridView1.RowsDefaultCellStyle.SelectionBackColor Color.FromArgb(3, 11, 23);dataGridView1.RowsDefaultCellStyle.SelectionForeColor Color.Cyan;dataGridView1.AlternatingRowsDefaultCellStyle.SelectionBackColor Color.FromArgb(20, 30, 50);dataGridView1.AlternatingRowsDefaultCellStyle.SelectionForeColor Color.Cyan;// 订阅选中变化以重绘边框dataGridView1.SelectionChanged (s, e) dataGridView1.Invalidate();// 默认选中第一行if(dataGridView1.Rows.Count 0)dataGridView1.Rows[0].Selected true;dataGridView1.DataError (s, ev) {ShellLine.WriteLine($DataGridView DataError: Row{ev.RowIndex}, Col{ev.ColumnIndex}, Msg{ev.Exception?.Message});ev.ThrowException false;};#endregion绘制鼠标悬浮外边框核心源码如下// 核心绘制方法在行绘制完成后额外绘制一个矩形边框 private void DataGridView1_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e) { // 第一行永远显示边框最新记录 bool isFirstRow (e.RowIndex 0); // 当前行被选中时显示边框 bool isSelected dataGridView1.Rows[e.RowIndex].Selected; // 鼠标悬浮的行显示边框 bool isHover (_hoverRowIndex e.RowIndex); // 只要满足任一条件即绘制 if (!isFirstRow !isSelected !isHover) return; if (!dataGridView1.Rows[e.RowIndex].Visible) return; Rectangle rect e.RowBounds; rect.X 1; rect.Width - 2; rect.Y 1; rect.Height - 2; using (Pen pen new Pen(Color.FromArgb(0, 160, 230), 2.5f)) { e.Graphics.DrawRectangle(pen, rect); } }控件轨迹移动特效以及落格动画效果private void AnimationTimer_Tick(object sender, EventArgs e) { if (_isMinimized) return; // 最小化时不更新 ListPackage completedPackages new ListPackage(); lock (packageLock) { foreach (var pkg in packages) { int step (int)(pkg.SpeedPxPerSec / 50.0); if (step 1) step 1; if (pkg.Phase MovePhase.Horizontal) { if (pkg.Label.Left pkg.HorizontalTarget.X) { int oldLeft pkg.Label.Left; pkg.Label.Left step; if (pkg.Label.Left pkg.HorizontalTarget.X) pkg.Label.Left pkg.HorizontalTarget.X; } if (pkg.Label.Left pkg.HorizontalTarget.X pkg.DropTriggered) { pkg.Phase MovePhase.Falling; double dx pkg.FallTarget.X - pkg.Label.Left; double dy pkg.FallTarget.Y - pkg.Label.Top; double distance Math.Sqrt(dx * dx dy * dy); double speed distance / FallDurationSec; speed Math.Max(10, Math.Min(500, speed)); pkg.SpeedPxPerSec speed; ShellLine.WriteLine($包裹{pkg.Id} 开始下落速度{speed:F1}px/s); } } else if (pkg.Phase MovePhase.Falling) { // 下落逻辑原样保持不变 int dx pkg.FallTarget.X - pkg.Label.Left; int dy pkg.FallTarget.Y - pkg.Label.Top; int fallStep step; if (fallStep 1) fallStep 1; if (Math.Abs(dx) fallStep Math.Abs(dy) fallStep) { pkg.Label.Left pkg.FallTarget.X; pkg.Label.Top pkg.FallTarget.Y; pkg.Phase MovePhase.Completed; CompletePackage(pkg); completedPackages.Add(pkg); } else { double distance Math.Sqrt(dx * dx dy * dy); double ratio fallStep / distance; int moveX (int)(dx * ratio); int moveY (int)(dy * ratio); if (moveX 0 moveY 0) { moveX Math.Sign(dx); moveY Math.Sign(dy); } pkg.Label.Left moveX; pkg.Label.Top moveY; } } } // 移除已完成的包裹 foreach (var pkg in completedPackages) packages.Remove(pkg); } }包裹和格口控件重合后高亮显示以及控件资源释放private void CompletePackage(Package pkg) { // 包裹消失并释放控件 pkg.Label.Visible false; this.Controls.Remove(pkg.Label); pkg.Label.Dispose(); // 目标格口变色亮绿色 Label chute pkg.TargetChute; chute.BackColor Color.LimeGreen; // 延迟恢复颜色0.5秒 // 若该格口已有恢复定时器先取消 if (chuteRestoreTimers.ContainsKey(chute)) { chuteRestoreTimers[chute].Stop(); chuteRestoreTimers[chute].Dispose(); chuteRestoreTimers.Remove(chute); } System.Windows.Forms.Timer restoreTimer new System.Windows.Forms.Timer(); restoreTimer.Interval 500; restoreTimer.Tag chute; restoreTimer.Tick RestoreChuteColor_Tick; restoreTimer.Start(); chuteRestoreTimers[chute] restoreTimer; // 累加重量到目标格口 int idx chuteLabels.IndexOf(pkg.TargetChute); if (idx 0 idx _chuteTotalWeights.Length) { _chuteTotalWeights[idx] pkg.Weight; decimal kg _chuteTotalWeights[idx] / 1000m; chuteLabels[idx].Text idx 24 ? $异常口\n{kg:F1} kg : $格口{idx 1}\n{kg:F1} kg; } _parcelPackageMap.TryRemove(pkg.Id, out _);//从映射字典中移除释放引用 // 更新日志安全调用 UpdateStatus($分拣成功{pkg.Id} → {chute.Name}, Color.Green); } private void RestoreChuteColor_Tick(object sender, EventArgs e) { System.Windows.Forms.Timer timer sender as System.Windows.Forms.Timer; timer.Stop(); Label chute timer.Tag as Label; if (chute ! null) { int idx chuteLabels.IndexOf(chute); if (idx 0) chute.BackColor chuteOriginalColors[idx]; // 恢复原始颜色 if (chuteRestoreTimers.ContainsKey(chute)) chuteRestoreTimers.Remove(chute); } timer.Dispose(); }最后如果你觉得这篇文章对你有帮助不妨点个赞支持一下你的支持是我继续分享只是的动力。如果有任何疑问或需要进一步的帮助欢迎随时留言。公众号会经常发一些实用的小工具和demo源码有兴趣的麻烦点个关注赠人玫瑰手留余香感谢您的关注期待和您一起探讨再会
返回列表