做工控和上位机的朋友应该都有体会:每次项目评审,功能都过了,界面总被甲方吐槽“这软件看着像个标配demo”。WinForm的界面确实容易被做成那个样子,但这锅真不该让WinForm一个人背。很多时候我们把默认字体、默认间距、默认分辨率适配拿过来直接交差,能不丑吗?我这些年用WinForm做了不少工业软件,从设备监控、参数配置到数据报表,踩了不少坑,也攒了不少开源库和自定义控件的经验。这篇就把“丑”的根源、开源库选型、高DPI适配、交互流畅度这几个核心问题一次讲透,照着做,你的WinForm界面至少在颜值和操作手感上可以直接上一个台阶。
1. 先说清楚:WinForm 不是不行,是被默认值拖累了
1.1 丑的三宗罪:字体、间距、分辨率
很多人一说WinForm就说老气、粗糙,其实WinForm本身只是个绘图框架,控件的最终呈现效果取决于你用没用对参数。我接手过不少老项目,打开一看基本都是三宗罪。
第一宗罪是字体。默认字体是Microsoft Sans Serif,8磅,在Win10、Win11的屏幕上显示又细又小,整个软件看起来像上世纪的产品。工业软件里信息密度本来就高,参数列表、状态信息、报警文本密密麻麻,这种细字体在屏幕上一放大全是锯齿感。解决办法倒很简单:全局把字体换成微软雅黑,字号提到9号或10号,标题栏用12号加粗。代码里写一次,所有控件继承父窗体字体,整个界面气质立刻不一样。
第二宗罪是间距。WinForm默认控件之间几乎没有什么留白,按钮贴按钮、文本框挤在标签旁边,视觉上就很“野”。工业软件界面信息量本来就大,没有合适的间距,人眼根本分不清层级。改造方法不是一个个控件去拖,而是重写布局:用TableLayoutPanel和FlowLayoutPanel做网格,控制Margin和Padding,把内容区做成有呼吸感的卡片式布局。
第三宗罪是分辨率适配。很多老项目压根没做DPI感知声明,在2K、4K高分屏上跑起来字体发虚、控件错位、图片模糊。笔记本1366x768的分辨率跑起来窗口超出屏幕。这个问题不是WinForm画不出来,而是开发者没告诉系统“我能自己适配”。后续第3章专门讲高DPI的处理方式,这个属于性价比最高的改造项。
1.2 为什么工业场景仍死守 WinForm
有个热搜词叫“工控wpf为何替代不了winform”,我在现场待过几年,感触很深。WPF的XAML声明式布局、数据绑定、动画效果确实比WinForm强太多,界面能做得非常漂亮。但工控软件的核心需求不是漂亮,而是稳定可控。
第一,工控上位机要跟PLC、串口设备、板卡、扫码枪、称重仪表打交道,这些设备的通信库和SDK绝大多数是C/C++或老式.NET写的,WinForm的控件直接拖过来就能用;WPF要引入Dispatcher、数据上下文、命令绑定,学习曲线一下子变陡。第二,现场工程师和维护人员对WinForm的右键菜单、属性窗口、资源管理器式的树形导航非常熟悉,改WPF后二次维护的成本很高。第三,WPF默认启动速度和内存占用都比WinForm大一些,在一些老的工控机上跑起来没有WinForm轻快。
说白了,WinForm不是被淘汰了,而是它太成熟、太稳了。我们真正需要做的,是在这个成熟框架上把“脸面”补上来,用开源库和现代前端思路把感官体验拉上去。下面的内容全部围绕这个目标展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开源控件库选型:颜值和效率可以同时要
2.1 主流开源 UI 库横向对比
先说结论:WinForm界面的美化,最省力的方式就是引入一套成熟的开源控件库,很多基础控件它已经帮你重新画好了。我用过的几款,各有侧重。
| 控件库 | 风格定位 | 依赖平台 | 适合场景 | 备注 |
|---|---|---|---|---|
| SunnyUI | 现代简约、浅色为主 | .NET Framework 4.0+ / .NET Core | 工控上位机、管理后台 | 主题色、字体全局配置,控件全,文档全 |
| HZHControls | 功能型、控件丰富 | .NET Framework 4.0+ | 工业控制、医疗设备、数据展示 | 有动画按钮、LED数码管、机器人状态控件 |
| MaterialSkin | Material Design风格 | .NET Framework 4.5+ | 希望有扁平化、卡片式效果 | 体积小,控件数量少 |
| ReaLTaiizor | 多种现代主题 | .NET Framework / .NET Core | 自定义主题需求多的项目 | 可选主题很多,但需要调试 |
| Ant Design Win | 阿里风格移植 | .NET 6+ | 后端管理系统 | 较新,适合新项目 |
我个人最常用的是SunnyUI和HZHControls。SunnyUI胜在整体感强,从窗体标题栏到按钮、下拉框、表格全部统一风格,适合快速把整个系统“翻新”;HZHControls则更强在工业控件,像仪表盘、管道图、LED显示,都是WinForm原装控件里没有的。
这里有个很容易踩的坑:千万别把一个项目里同时引SunnyUI和HZHControls,两个库都有UIDropDown、UIButton之类带UI前缀的控件,命名空间和类名冲突会让你改到怀疑人生。一次项目只选一个主库,不够的用自己画的自定义控件补。
提示:如果项目已经有大量原生WinForm窗体,最好选SunnyUI这类“不改基类也能用”的库。它既有继承自UC基础控件的API,也有适配原窗口的扩展方式,改动量最小。HZHControls很多控件需要继承它的基类窗体,重构成本高一些。
2.2 接入后立竿见影的全局设置
以SunnyUI为例,接入非常简单,NuGet搜SunnyUI安装,然后在Program.cs里做全局初始化:
csharp复制using Sunny.UI;
namespace MyApp
{
internal static class Program
{
[STAThread]
static void Main()
{
// .NET 6+ 项目用这行,老项目写成 Application.EnableVisualStyles();
ApplicationConfiguration.Initialize();
// 设置全局主题色和自定义字体
UIStyles.InitColor(Color.FromArgb(48, 68, 98), UIStyle.Custom);
UIStyles.InitFont(new Font("微软雅黑", 10F));
Application.Run(new MainForm());
}
}
}
这行InitFont是重点。它会把项目里所有SunnyUI控件的字体统一改成微软雅黑10号,连窗体标题栏里的文本、按钮里的文字都会跟着变。相比一个个改属性,这种全局配置的效率提升是肉眼可见的。
接入后,窗体的基类从Form改成UIForm,按钮用UIButton,文本框用UITextBox,表格用UIDataGridView。界面风格马上统一,不再是一堆灰色方块的拼凑。
2.3 曲线图表与仪表盘:工业软件离不开的可视化库
工业软件最常见的可视化需求就是趋势曲线和仪表盘。WinForm自带控件没有曲线图,很多人会想到用第三方商业控件,但开源方案完全够用。
- ScottPlot:轻量,画布式渲染,性能好,自带鼠标缩放、游标,适合实时曲线和频域分析,.NET 6+ 和 .NET Framework 都能用。
- LiveCharts2:图表类型丰富,支持折线、柱状、饼图、热力图,动画效果好,适合做报表页。
- ZedGraph:老牌曲线库,偏向科学绘图,中文资料多,很多老工控项目里都在用,缺点是界面风格偏老。
- OxyPlot:跨平台,支持多种输出格式,代码控制灵活。
我推荐新项目优先ScottPlot。它的API调用简单:
csharp复制var plt = formsPlot1.Plot;
plt.Title("温度趋势");
plt.XLabel("时间");
plt.YLabel("温度(°C)");
plt.AddSignal(values, sampleRate: 1); // values是double数组
formsPlot1.Refresh();
实时刷新时不要每毫秒都Refresh,用Timer控制在200~500ms刷新一次,曲线依然流畅,CPU占用率很低。仪表盘方面,HZHControls里提供了 UCRoundProcess、UCircularMeter 这类现成控件,做转速、液位、压力显示直接拖过来改一下范围就可以用。
3. 高DPI与自适应布局:解决“看着糙”的第一步
3.1 高DPI感知设置:两步改彻底
WinForm在高分屏上模糊,根本原因是进程没有声明DPI感知,Windows把它当普通程序做位图拉伸。解决也就两个步骤。
第一步,在项目里添加应用程序清单文件app.manifest,把下面这段配置放进去:
xml复制<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
</windowsSettings>
</application>
PerMonitorV2表示每个显示器单独适配DPI,比单纯true更现代。当窗口在两个不同缩放比例的显示器之间拖动时,系统会通知程序按新的DPI重新布局,不再模糊拉伸。
第二步,如果项目是.NET Framework 4.7及以上,还要在App.config里加一段:
xml复制<appSettings>
<add key="EnableWindowsFormsHighDpiAutoResizing" value="true" />
</appSettings>
这里要提醒一下:.NET Framework 4.6及以下不支持PerMonitorV2,升级项目或者手动调用SetProcessDpiAwarenessContext。对于老框架,也可以调用user32.dll的SetProcessDPIAware(),但没有逐显示器感知,只能做到第一屏缩放正确。
3.2 布局自适应与字体缩放
做好声明只是第一步,第二步是让布局能跟着DPI变。WinForm里的AutoScaleMode有三种常用选择:None、Dpi、Font。我的建议是统一用Dpi,并且把所有窗体都设为同一个值。如果混用,窗体在不同缩放比之间切换时,控件会明显跑位。
工业软件里最怕的布局问题是窗口在1366x768的老笔记本上放不下。很多同行做界面喜欢用绝对坐标,窗体和控件都是固定大小,一换分辨率就要改代码。正确做法是:
- 窗体不用固定Size,而是先设MinimumSize和MaximumSize;
- 内容区用TableLayoutPanel做两列或三列网格,列用百分比,行用AutoSize或Fill;
- 按钮栏用FlowLayoutPanel + Dock=Bottom,左右间距用控件自身的Margin控制;
- 数据表格用Dock=Fill,让它自动填充剩余区域。
有个搜索热词叫“vs winform界面的高宽和高过长,怎么处理”,其实就是窗口尺寸适配问题。如果是内容太多纵向超过屏幕,用Panel包一层,设AutoScroll=true,滚动区域里的控件按设计尺寸摆放,超出部分用滚动条查看。如果是数据表格列太多横向超过屏幕,就把表格嵌在AutoScroll的Panel里,或者用DataGridView的列冻结,把关键列锁在左边。
3.3 不同分辨率电脑的兼容调试
做高DPI适配后,一定要在几种常用分辨率下实测:1366x768、1920x1080、2560x1440,以及系统缩放百分比125%、150%的情况。很多问题在自己开发机上不出现,一放到现场工控机上就露馅,多半就是因为开发机是1080P 100%缩放,现场是768P 125%缩放。
没有多台显示器的同学可以开个显示模拟器,Windows设置里的“缩放与布局”可以直接改;也可以用Visual Studio的模拟器项目,选不同屏幕尺寸跑。另外,在窗体的Resize和DpiChanged事件里打日志,能看到不同缩放比下窗体的实际工作区尺寸变化,排查起来更直观。
我踩过的一个典型坑:程序在125%缩放的电脑上启动,窗体的宽度明明设置成1200,运行起来实际只显示1200的一半宽左右,控件全部挤到左上角。查了一圈,就是Manifest没加PerMonitorV2,系统默认按位图放大了整个窗口。加完声明,再配合AutoScaleMode.Dpi和TableLayoutPanel,这个问题就彻底消失了。
4. 界面骨架升级:左侧菜单、TreeView、PropertyGrid 的实战改造
4.1 左侧菜单:从TreeView到侧边栏的完整改法
工业软件最常见的导航就是左侧菜单。原生做法是用TreeView,但它默认样式确实不美观,节点选中效果不明显,展开箭头配上一堆黑色文字,在现在的审美下很难看。常用方案有几种。
第一种,保留TreeView,改成自绘模式。把DrawMode设为OwnerDrawText,然后在DrawNode事件里自己画背景、文字和选中色块。这样可以做到节点圆角、选中高亮,但TreeView的展开箭头样式和缩进还是老样子,适合改动量最小的情况。
第二种,用第三方库的侧边栏控件。SunnyUI提供UCNavMenu和UCMenu,HZHControls提供UCMenuTab,都是现成的左侧菜单。UI风格统一,支持图标、分组、选中变色、折叠动画,这是我推荐的方式。接入就一行代码挂事件的事,不用自己写。
第三种,自己做一个“假菜单”。用几个Button垂直排列,点击时切换右侧Panel的内容,或者用RadioButton + Panel容器模拟选中态。这种方式对布局控制最强,适合菜单项数量固定且不多的场景。
选哪种,取决于菜单量。菜单项少于10个,用按钮列最适合;菜单项经常动态增删,用TreeView自绘;想要专业感又不想自己画,直接用SunnyUI的UCNavMenu。
4.2 TreeView 美化与高效加载
如果一定要用TreeView,至少把它弄得像个现代控件。原生TreeView的问题主要有三个:节点文字离图标太近、选中背景是蓝色方块、展开收起没有平滑感。
自绘的核心代码大概这样:
csharp复制treeView1.DrawMode = TreeViewDrawMode.OwnerDrawText;
treeView1.FullRowSelect = true;
treeView1.ItemHeight = 26;
treeView1.DrawNode += (s, e) =>
{
// 先画背景,保证文字不被残留覆盖
var bgColor = (e.State & TreeNodeStates.Selected) != 0
? Color.FromArgb(48, 68, 98)
: Color.White;
using (var bgBrush = new SolidBrush(bgColor))
{
e.Graphics.FillRectangle(bgBrush, e.Bounds);
}
// 画文字,左边预留图标空间
var textColor = (e.State & TreeNodeStates.Selected) != 0
? Color.White
: Color.FromArgb(64, 64, 64);
TextRenderer.DrawText(
e.Graphics,
e.Node.Text,
treeView1.Font,
new Rectangle(e.Bounds.Left + 24, e.Bounds.Top, e.Bounds.Width - 24, e.Bounds.Height),
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left);
};
当节点超过几百个时,要注意用懒加载。不要一次性把所有子节点都加到TreeView里,可以在节点首次展开时再加载子节点。事件是 BeforeExpand,里面判断节点是否已经加载过,没有就创建子节点并填充图标。机械设备树、工艺配方树这种动辄上万个节点的数据结构,必须用懒加载,否则界面卡几秒是常事。
图标方面,给TreeView设置ImageList,然后Node的ImageIndex指向对应图标。工业软件常见的文件夹、设备、报警、运行等图标,建议用16x16的,和ItemHeight 26搭配比较协调。
4.3 PropertyGrid 只读改可编辑:让参数面板真正可用
有网上的朋友问“winform的propertygrid只能查看不能修改怎么现实”,这个问题很典型。PropertyGrid显示一个对象参数时,如果对象属性没有public setter,或者属性被 [ReadOnly(true)] 标记,界面上的值就是灰色不可编辑状态。
最简单粗暴的办法,是在数据类里给属性加set:
csharp复制public class DeviceConfig
{
public string DeviceName { get; set; }
public int Port { get; set; }
}
如果属性确实不能暴露set,还需要在PropertyGrid上可编辑,可以用一个常用套路:用反射拿到只读的PropertyDescriptor,在PropertyGrid选中对象前,把它替换成自定义的可写描述。核心代码片段:
csharp复制public static object MakePropertiesEditable<T>(T instance)
{
var provider = new ReadOnlyRemover(typeof(T));
TypeDescriptor.AddProvider(provider, instance);
return instance;
}
public class ReadOnlyRemover : TypeDescriptionProvider
{
public ReadOnlyRemover(Type type) : base(TypeDescriptor.GetProvider(type)) { }
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
var descriptor = base.GetTypeDescriptor(objectType, instance);
return new EditableTypeDescriptor(descriptor);
}
}
EditableTypeDescriptor里重写GetProperties,遍历每个PropertyDescriptor,把IsReadOnly返回true的替换成新的可写描述。这个方案适合那种“底层数据类不方便改set,但界面层需要编辑参数”的场景。
另外一个影响使用体验的小细节:PropertyGrid的HelpVisible和ToolbarVisible。默认会显示顶部工具栏和底部说明区,会挤占大块空间,工业软件里建议把HelpVisible设成false,工具栏留默认,或者把PropertyGrid放到一个可折叠的Panel里,需要修改参数的时候展开,平时收起来。
4.4 控件尺寸溢出与动态获取显示区域
“winform由bitmap获取指定大小”这个问题,本质是动态决定控件尺寸时,需要先知道当前工作区大小。工业软件里常见场景是:根据屏幕分辨率调整主界面大小,或者从摄像头采集的Bitmap里截取指定区域显示。
获取可用显示尺寸最靠谱的方式不是 Screen.PrimaryScreen.Bounds,而是 Screen.FromControl(this).WorkingArea,它排除了任务栏占用区域:
csharp复制var area = Screen.FromControl(this).WorkingArea;
this.Width = (int)(area.Width * 0.9);
this.Height = (int)(area.Height * 0.9);
this.StartPosition = FormStartPosition.CenterScreen;
在主界面启动时读取一次WorkingArea,然后设置窗体的尺寸,能确保在任何分辨率下都不超出屏幕。给图片框设置图片时,如果需要从Bitmap中获取指定大小区域,可以用Graphics.DrawImage加裁剪:
csharp复制using (var src = new Bitmap(sourcePath))
{
var rect = new Rectangle(x, y, width, height); // 指定区域
var crop = new Bitmap(width, height);
using (var g = Graphics.FromImage(crop))
{
g.DrawImage(src, new Rectangle(0, 0, width, height), rect, GraphicsUnit.Pixel);
}
pictureBox1.Image?.Dispose();
pictureBox1.Image = crop;
}
这样就能把大图里的指定区域提取出来显示,常用于工业相机拍照后的局部检查或图像处理。
5. 交互流畅度:不卡界面、不崩UI的线程与刷新方案
5.1 Invoke 跨线程更新的正确姿势
工业软件最大的性能痛点通常是UI卡死:设备读取数据、通信超时、大量日志刷新时,界面一动不动,操作人员点按钮没反应。UI卡死的根源几乎都是同步阻塞——通信和界面显示在同一个线程里跑。
跨线程访问控件的标准做法是Invoke。.NET的控件不是线程安全的,在非UI线程里直接改控件属性会抛 InvalidOperationException。但很多通信回调会带数据,所以必须通过Invoke把更新动作发回UI线程。
这里要明白Control.Invoke和Control.BeginInvoke的区别。Invoke是同步,调用线程会阻塞直到UI线程执行完委托;BeginInvoke是异步,调用线程立即返回,UI线程空闲时再执行委托。实时性要求高的控制界面里用Invoke,但如果每秒有大量更新请求,用BeginInvoke更好,不会让通信线程积压。
推荐的写法:
csharp复制private void UpdateStatus(string msg)
{
if (this.InvokeRequired)
{
// 因为BeginInvoke是异步,通常不会阻塞通信线程
this.BeginInvoke(new Action<string>(UpdateStatus), msg);
return;
}
lblStatus.Text = msg;
}
InvokeRequired判断是否跨线程,如果跨线程就转回UI线程再递归调用。这种方法比直接在事件回调里写Invoke更整洁。
5.2 async/await 替代裸线程
现在开发WinForm建议直接用async/await,而不是手动创建Thread或ThreadPool.QueueUserWorkItem。async/await本质是状态机调度,不会创建新线程,而是复用线程池线程,并且能自动捕获UI线程的SynchronizationContext,在await之后回到UI线程继续执行。
实际场景:读取PLC数据通常是一两百毫秒的阻塞操作,放在UI线程里点一下按钮就卡一下。改成async/await:
csharp复制private async void btnReadData_Click(object sender, EventArgs e)
{
btnReadData.Enabled = false;
try
{
var data = await Task.Run(() => ReadPlcData());
textBox1.Text = data;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
btnReadData.Enabled = true;
}
}
注意两点:一是 async void 只用于事件处理器,其他方法用 async Task;二是button的Enabled在异步期间要禁用,防止用户重复点击导致多个读取任务并发执行。
对于需要长时间轮询的采集任务,不要直接在await后面写while(true)。可以正常地用CancellationToken控制停止,或者用一个Timer定时触发读取。Timer的Tick在UI线程执行,间隔设成500ms以上,读取方法用async void配合await,基本能保证界面流畅。
5.3 大数据量表格与曲线的流畅渲染思路
数据表格在工业软件里动辄几万条记录,DataGridView默认全量加载数据会导致滚动和刷新都卡。解决方案是VirtualMode虚拟模式。开启 dataGridView1.VirtualMode = true,然后不直接设置DataSource,而是通过CellValueNeeded事件按需返回当前行数据。这样DataGridView只渲染当前可见行,数据量再大也不会卡。
曲线图实时更新时,如果每秒钟加几十个点,几百个点后可以做个简单抽样,例如只保留最近2000个点,DisplayRange只显示最近500个点。ScottPlot里有 plt.AxisAuto() 或者用 plt.SetAxisLimits 手动控制显示范围,避免每次更新都重新算全图范围,性能提升很明显。
批量刷新UI时,可以短暂挂起控件的布局:
csharp复制this.SuspendLayout();
try
{
foreach (var item in items)
{
// 更新控件
}
}
finally
{
this.ResumeLayout(true);
}
这个方法用来防止每次修改控件属性都触发一次重绘,在加载大量节点或数据时省很多性能。
关于“winform 反射 触发click事件”和“c# winform不同界面共享数据”这两个常用场景,简单补充一下:触发按钮的BeginInvoke可以通过反射拿字段再调用PerformClick方法,但业务上更推荐用事件委托;不同窗体共享数据可以创建静态数据类,或者用事件发布订阅,把“显示数据”和“业务数据”解耦开。
6. 开源库接入与部署避坑:从NuGet到安装包
6.1 引入第三方库时最常见的三个坑
第一个坑是版本冲突。两个开源库可能依赖不同版本的同一个底层DLL,编译期一切正常,运行期报 FileNotFoundException 或 TypeLoadException。解决办法是打开App.config,看生成的bindingRedirect节点,确认依赖统一版本。用NuGet的“控制台”执行 Update-Package -Reinstall 也可以强制重装所有包。
第二个坑是目标框架不匹配。有些现代开源库要求.NET 6以上,你的老项目如果是.NET Framework 4.6.2,引进去直接编译失败。选库之前先看清楚它的目标平台。
第三个坑是命名空间冲突。前面说过SunnyUI和HZHControls会撞类名,另外一些库和原生控件命名空间也撞。Minimize在using时用别名区分:
csharp复制using Sunny = Sunny.UI;
using HZH = HZH_Controls;
多个库混用本身就是设计问题,尽量别这么干。一定要混用的话,至少要做到用别名隔离,并确认两个库不会同时破坏全局UI风格。
6.2 自定义控件放不进工具箱怎么办
新装了开源库,工具箱里一直看不到控件,这是大概率会遇到的。最常见的原因是工具箱没有开启“自动填充”。Visual Studio里右键工具箱,选择“选择项”,在“.NET Framework组件”页签里点“浏览”,找到bin目录下的控件DLL,选中确定。
如果选择了DLL但控件列表是空的,检查DLL是不是放在了调试输出目录(bin\Debug或bin\Release)里,以及文件是否被编译成AnyCPU。很多时候项目生成的是x86或x64,工具箱的进程是AnyCPU,会判断不了。
还有一个习惯:开发自定义控件时,给自定义控件类加上 [ToolboxBitmap(typeof(MyControl))] 和 [DefaultProperty("Text")] 特性,设计器里的使用体验会好很多。
6.3 安装包制作与运行时依赖
部署工控软件,不能只想着拷贝exe过去。开源库的DLL、字体文件、配置文件、日志目录都要一并处理好。
制作安装包最顺手的方案是Inno Setup,免费、脚本简洁、打包体积小。以《c#的winform如何制作安装包》这个问题为例,用Inno Setup写个脚本:
ini复制[Setup]
AppName=我的工控软件
AppVersion=1.0.0
DefaultDirName={pf}\MyApp
OutputDir=Output
OutputBaseFilename=MyAppSetup
[Files]
Source: "bin\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\我的工控软件"; Filename: "{app}\MyApp.exe"
编译的时候注意发布形态。如果是.NET Framework项目,目标机器上需要装好对应版本的.NET Framework;如果是.NET 6/8自包含发布,可以直接把运行库一起打进去。项目文件里设置 <SelfContained>true</SelfContained> 就是自包含模式,绿色免安装,适合工控机。
如果是Visual Studio自带的Installer Projects,功能虽然简单,但生成.msi给企业IT部署也很方便。唯一要注意的是,把项目主输出和前几章提到的第三方依赖DLL都包含进去,别只选主输出,否则有些DLL会漏打包。
注意:安装包设置安装目录时,尽量用
{pf}(Program Files)或者用户目录,不要写死成C盘根目录。工控软件经常要读配置文件,写死在系统盘目录会触发UAC权限问题,导致软件没有权限修改配置,运行时报“拒绝访问”。
7. WPF 那么好,为何工控现场还在用 WinForm
7.1 稳定性、老代码与上手成本
这个话题值得单独聊聊。WPF在界面表现力上确实比WinForm强一个量级,特别是数据模板、动画、样式,工业软件在很多可视化页面上做出来会非常漂亮。但工控项目里,我见到的绝大多数还是WinForm。
核心原因有几个。一是老代码资产:不少公司积累了十年甚至更久的WinForm窗体库、控件库、通信组件,全部迁到WPF不现实,周期长、风险大;二是现场维护人员更熟悉WinForm的调试方式,出问题能快速定位;三是很多工业通信库、硬件SDK的示例代码都是基于WinForm写的,直接照抄就能跑,WPF里还得研究怎么把同步阻塞操作迁移到异步模式。
7.2 我的推荐组合:默认框架 + 开源UI库 + 高DPI + 异步刷新
做了这么多年WinForm项目,我的推荐组合就是四件套:默认框架+一个开源UI库+高DPI适配+异步刷新。
- 默认框架定稳定底线;
- 开源UI库解决基础控件的颜值问题;
- 高DPI适配保证界面在不同显示器上不变形;
- 异步刷新让操作不卡、不假死。
这四个加起来,再配合一些小细节,比如启动画面用无边框窗体做一张渐变背景图、主窗体打开加一个淡入淡出效果、按钮鼠标悬停变色,整体体验已经完全不输很多WPF应用了。别小看这些“非功能需求”,对客户来说,界面流畅、看着舒服,直接决定他对软件质量的判断。
7.3 最后分享几个提升WinForm开发效率的小窍门
第一,做一套自己的WinForm模板项目,里面把字体、主题色、高DPI配置、日志模块、异常处理全部预置好,新项目直接复制改名字。我自己维护的模板项目至少省了每个项目最初两天的环境搭建时间。
第二,多用UserControl封装。设备状态面板、参数编辑区、报警列表这种常见区域,都做成UserControl,哪个窗体需要就往里拖,改一处全局生效。
第三,凡是超过50毫秒的操作,全部走异步。日志、通信、数据库读写、文件读写,没有例外。这条规则坚持下来,你的WinForm软件基本就不会再被吐槽“卡”了。
我在实际项目里感受最深的就是:WinForm不是不能做得好,而是很多人没花时间把基础工程搭对。字体、间距、分辨率适配、异步刷新,这些在项目一开始就定好规范,后面每个窗体的开发就都是在这套规范上叠功能,效率高,交付的软件也体面。下一次再听到有人说“WinForm丑”,你可以直接把这份实践清单甩过去。
