如何将WinForm.NET代码迁移到Blazor WASM平台上

如何将WinForm.NET代码迁移到Blazor WASM平台上

南京都昌信息科技有限公司 袁永福 2025-12-3

1.前言

自从基于MS .NET Framework的WinForm.NET开发模式的诞生,20多年来IT业界开发了大量的WinForm.NET软件。但近几年,随着toB软件快速从CS模式转变为BS模式,叠加国产信创的泰山压顶。大量的WinForm.NET软件遇到生死局,数千万行C#代码可能会废弃,大量开发组织和用户面临重大价值损失。

我们也遇到这个难题,在过去2年的时间,我们花费了很大的精力,成功的将DCWriter编辑器控件WinForm.NET版迁移到Blazor WASM平台上,完美的解决了这个生死难题。在此说明其中的技术原理,展示我们是如何做到的,希望能给遇到类似情况的开发者提供一个参考意见。

2.基本原理

下图是WinForm.NET程序的基础架构图:

在这个架构图中,System.Windows.Forms.Control和System.Drawings.Graphics是最核心的类型。System.Windows.Forms.Control类型用于将鼠标键盘事件发送给DCWriter核心模块来完成用户互动的操作。而DCWriter核心模块则使用System.Drawings.Graphics类型来绘制用户界面,使得软件和用户互动,形成一个闭环。

参考WinForm.NET程序的基本原理,我们摸索出如下的程序架构:

要实现这个架构,核心是模拟出System.Windows.Forms.Control和System.Drawing.Graphics类型,只要模拟得足够像,则DCWriter核心模块无需大改就能运行起来。这样DCWriter核心模块和用户操作也能形成闭环,达成实时互动。这就是将WinForm.NET程序迁移到Blazor WASM平台上的基本原理。

3.实现过程

按照这个架构图,我们如下进行分步实现的:

3.1.模拟System.Windows.Forms.Control类型

Blazor WASM架构中是没有System.Windows.Forms.dll的,因此我们来创建一个C#类型 public class Control{}。该类型包含以下成员:

这个成员函数完整模拟了核心模块所依赖的标准System.Windows.Forms.Control的成员。

在代码中使用了很多Blazor WASM标准库中不存在的类型,比如Color、Rectange、Cursor、KeyEventArgs、KeyPressEventArgs、MouseEventArgs、PaintEventArgs之类的,都需要我们扩散定义。

然后我们跟着定义周边的类型,比如ScrollableControl、UserControl之类的。由于我们的DCWriter编辑器控件是派生自UserControl,于是一部分DCWriter核心模块形式上开始迁移过来了。

3.1.1.模拟键盘事件

WinForm.NET程序是靠重写Control.OnKeyUp/OnKeyPres/OnKeyDown虚函数来实现键盘事件。事件参数类型是 System.Windows.Forms.KeyEventArgs/KeyPressEventArgs。

首先定义一个鼠标事件转发器。其代码如下:

这是一个标记了[JSInvokeable]的函数,这个函数接受JS端传过来的事件参数,将其转换为一个System.Windows.Forms.KeyEventArgs,然后根据事件名称触发控件的键盘事件。

然后在JavaScript端,我们对一个<input type=text>绑定了键盘事件处理:

在这个键盘事件处理的JS代码中,我们使用invokeMethod()通过JSInterop调用了在C#端定义的EditorHandleKeyEvent()函数,并将事件参数传递给C#端。

通过这种方式,我们打通了"HTML元素键盘事件->转换器->KeyEventArgs->Control.OnKeyDown->DCWriter核心模块"的事件传递通道。

通过类似的方式,我们打通了鼠标点击、移动、拖拽事件的传递通道。

3.1.2.模拟Control.Invalidate(Rectangle)

在 WinForm.NET程序中,Control.Invalidate()也是一个非常重要的成员方法需要模拟出来,为此我们定义以下方法:

1.定义C#方法void Control.Invalidate( Rectangle )方法,则该方法内部使用一个List<Rectangle>来存储多个无效矩形区域,并进行矩形区域的合并操作,减少重绘的工作量。然后通过JSInterop调用无参数的JS函数NeedUpdateView()。

2.定义C#方法[JSInvokeable]public byte[] GetInvalidateViewData(),该函数检索无效矩形列表,获得第一个无效矩形作为ClipRectangle,然后创建Graphcis对象,创建一个System.Windows.Forms.PaintEventArgs对象,调用核心模块的绘图模块来绘制图形,返回包含绘图指令的字节数组,删除第一个无效矩形对象。如果没有任何无效矩形区域,则返回null。

3.定义JS方法 function NeedUpdateView()函数,该函数使用window.setTimeout()来延时调用另外一个JS函数 function DrawContent()。

4.定义JS方法function DrawContent(),该函数通过JSInterop调用C#函数GetInvalidateViewData(),尝试获得绘图指令字节数组。如果存在则调用JS类PageCotentDrawer在HtmlCanvasElement上的指定区域绘制图形。

通过这4个方法以Control.Invalidate() 牵头串联在一起,共同完成用户界面的主动局部重绘的功能。

3.2.模拟System.Drawing.Graphcis类型

我们在C#端定义了Graphcis类型,其包含的成员如下所示:

在各个绘图函数内部,会将绘图指定和使用的参数值转储到一个内存字节流中。比如对于DrawLine()其代码如下:

这样当所有的绘图操作完成,Graphics内部一结算,就可以获得一个包含绘图指令的字节数组,然后返回给JS端。

3.2.1.模拟Graphics.MeasureString()

这里还有一个非常重要的成员方法MeasureString()需要进行模拟,这个方法用于测量字符的显示宽度,直接决定了文档的排版结果。由于文档中可能包括十万个字符,如果依赖浏览器的measureText(),则由于频繁的JSInterop操作降低性能,而且各个浏览器之间运算结果可能不一致。为此我们采用如下方法:

1.使用WinForm.NET开发一个专用工具解析字体TTC/TTF文件,提取所有字符的宽度数值。

2.将字符的Uncode编码根据相同的字符宽度来划分区域,以此将这些宽度数值高度压缩为"字体快照信息"。例如宋体字体文件simsun.ttc有18MB大小,由于是等宽字体,其快照信息仅 1KB。

3.由于快照信息很小,所以我们干脆将快照信息硬编码到程序中,为了方便维护,我们放置到JS中,其代码如下图所示。

4.基于这些字体快照信息,我们就可以模拟出MeasureString()。实践证明这个方法的计算速度非常快,而且其计算结果与原生MeasureString()的计算结果高度一致。

3.2.2.打印

我们会在JS中使用window.print()方法来执行打印,但打印HtmlCanvasElement会出现结果模糊的问题,这是由于打印机DPI远超显示器DPI而导致。因此我们使用SVG的模式进行高清打印。

为此,我们在C#端使用一个SVG指令翻译器来实现该功能。对于Graphics新增SVG打印模式。比如对于Graphics.DrawLine(),当Graphics处于SVG打印模式,则输出的不是二进制数据,而是输出SVG代码,例如<line x1="74" y1="121.5" x2="720" y2="121.5" stroke="Black"></line>。最后将SVG代码字符串传递到JS端,然后使用动态创建SVG元素来承载这些SVG代码,实现高清打印。

3.2.3. JS端DCBinaryReader类

我们定义了一个JS类 class DCBinaryReader {}。它在DataView的基础上实现了一个向前的二进制数据读取器。用于简化后续操作。

3.2.4. JS端PageContentDrawer类

我们定义了一个JS类class PageContentDrawer{}。它获得C#端Graphics对象生成的二进制数组,使用DCBinaryReader封装一下,然后在一个HTML的CANVAS元素上进行绘制。主体代码如图:

在这个循环体中,首先读取绘图指令编号,然后在绘图函数集中获得编号的绘图函数,然后进行调用。比如对于5号指令,这是绘制线段的功能,其功能代码如下:

在这个函数中,程序首先获得int16数据,这是画笔对象编号,从this.PenTable中获得画笔对象。然后使用DrawLine函数来绘制线段。这里调用了4次ReaderInt32(),这是获取线段的坐标信息,也就是x1,y1,x2,y2。

这样,我们就能将C#端Graphis.DrawLine()转换为JS端的canvas.drawLine(),实现了在一个HTMLCanvasElement上绘制图形。

至此,我们就模拟出System.Drawing.Graphics类型。

4. 最终效果

经过上述操作,我们成功的将DCWriter编辑器的WinForm.NET版本迁移到Blazor WASM平台上,实现了一个纯前端的符合信创的编辑器组件。一个文档在WinForm.NET版本的显示如下图所示:

同一个文档在谷歌浏览器中的显示效果如下图所示:

这个文档在FireFox浏览器中的显示如下图所示:

可以看出,同一个文档,在三种平台中排版和显示的结果完全一致,其键盘和鼠标事件处理行为也高度一致,而且通过了一些国产操作系统厂家的适配认证。这样的结果达到我们的预期,让我们的产品有幸能继续给客户带来持续的价值。