实例介绍
【实例简介】
【实例截图】
【核心代码】
using System; // 提供事件和基础类型。
using System.Drawing; // 提供界面颜色、字体及图标绘制类型。
using System.IO; // 提供流程文件读写功能。
using System.Linq; // 提供文档数据校验所需的集合查询。
using System.Text; // 提供 UTF-8 文本编码类型。
using System.Web.Script.Serialization; // 提供 .NET Framework 内置 JSON 序列化器。
using System.Windows.Forms; // 提供 WinForms 主界面控件类型。
namespace WorkflowDesigner // 隔离应用程序主界面类型。
{ // 开始定义主界面命名空间。
public sealed class MainForm : Form // 定义流程设计器主窗体。
{ // 开始定义主窗体成员。
private readonly WorkflowCanvas canvas; // 保存中心流程设计画布。
private readonly PropertyGrid propertyGrid; // 保存右侧对象属性编辑器。
private readonly ListView toolbox; // 保存左侧可拖放节点工具箱。
private readonly ToolStripButton selectButton; // 保存选择模式切换按钮。
private readonly ToolStripButton connectButton; // 保存连线模式切换按钮。
private readonly ToolStripStatusLabel statusLabel; // 保存底部操作提示文本。
private readonly JavaScriptSerializer serializer; // 保存 JSON 文档序列化器。
private readonly ScriptRuntime scriptRuntime; // 保存 Roslyn C# 脚本运行服务。
private string currentFile; // 保存当前文档对应的磁盘路径。
private bool dirty; // 标记当前文档是否存在未保存修改。
public MainForm() // 构建并连接流程设计器界面。
{ // 开始初始化主窗体。
Text = "工作流程图设计器"; // 设置窗口标题。
StartPosition = FormStartPosition.CenterScreen; // 在屏幕中央首次显示窗口。
MinimumSize = new Size(1000, 650); // 防止布局缩小到不可操作尺寸。
Size = new Size(1380, 820); // 提供舒适的默认设计空间。
KeyPreview = true; // 允许窗体优先处理删除和取消快捷键。
serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }; // 支持保存较大的流程文档。
scriptRuntime = new ScriptRuntime(); // 创建可复用的 Roslyn 脚本运行服务。
canvas = new WorkflowCanvas { Dock = DockStyle.Fill }; // 创建填满中心区域的画布。
propertyGrid = new PropertyGrid { Dock = DockStyle.Fill, HelpVisible = true, ToolbarVisible = true }; // 创建带说明区的属性面板。
toolbox = CreateToolbox(); // 创建可发起拖放的节点工具箱。
ToolStrip toolStrip = CreateToolStrip(out selectButton, out connectButton); // 创建顶部命令工具栏。
statusLabel = new ToolStripStatusLabel(); // 创建可动态更新的状态标签。
StatusStrip statusStrip = new StatusStrip(); // 创建底部状态栏容器。
statusStrip.Items.Add(statusLabel); // 将提示标签加入状态栏。
SplitContainer outerSplit = CreateWorkspace(); // 创建工具箱、画布和属性区布局。
Controls.Add(outerSplit); // 将主要工作区加入窗体。
Controls.Add(statusStrip); // 将状态栏加入窗体底部。
Controls.Add(toolStrip); // 将工具栏加入窗体顶部。
MainMenuStrip = CreateMenu(); // 创建菜单并启用标准快捷键。
Controls.Add(MainMenuStrip); // 将主菜单加入窗体控件集合。
WireEvents(); // 连接界面交互事件。
NewDocument(false); // 创建初始空白流程文档。
} // 结束初始化主窗体。
private ListView CreateToolbox() // 创建并配置节点工具箱列表。
{ // 开始创建节点工具箱。
ListView list = new ListView(); // 创建工具箱列表控件。
list.Dock = DockStyle.Fill; // 让列表填满左侧面板。
list.View = View.Tile; // 使用便于拖动的大项目样式。
list.TileSize = new Size(170, 50); // 设置舒适的工具项尺寸。
list.MultiSelect = false; // 限制每次只能拖动一种节点。
list.HideSelection = false; // 保持当前工具项的视觉反馈。
list.BackColor = Color.FromArgb(248, 250, 252); // 使用浅色工具区背景。
list.Items.Add(CreateToolItem("开始", NodeKind.Start)); // 添加开始节点工具。
list.Items.Add(CreateToolItem("处理", NodeKind.Process)); // 添加处理节点工具。
list.Items.Add(CreateToolItem("判断", NodeKind.Decision)); // 添加判断节点工具。
list.Items.Add(CreateToolItem("C# 脚本", NodeKind.Script)); // 添加 Roslyn 动态脚本节点工具。
list.Items.Add(CreateToolItem("结束", NodeKind.End)); // 添加结束节点工具。
return list; // 返回配置完成的工具箱。
} // 结束创建节点工具箱。
private static ListViewItem CreateToolItem(string text, NodeKind kind) // 创建携带节点类型的工具项。
{ // 开始创建工具项。
ListViewItem item = new ListViewItem(text); // 使用中文节点名称创建项目。
item.Tag = kind; // 保存拖放时需要的节点类型。
item.ToolTipText = "拖到中间画布创建" text "节点"; // 提供具体操作提示。
return item; // 返回配置完成的工具项。
} // 结束创建工具项。
private ToolStrip CreateToolStrip(out ToolStripButton select, out ToolStripButton connect) // 创建常用命令工具栏。
{ // 开始创建工具栏。
ToolStrip strip = new ToolStrip(); // 创建顶部工具栏容器。
strip.GripStyle = ToolStripGripStyle.Hidden; // 隐藏不必要的拖动手柄。
strip.Padding = new Padding(6, 4, 6, 4); // 增加工具按钮周围留白。
strip.Items.Add(CreateCommandButton("新建", (sender, args) => NewDocument(true))); // 添加新建文档命令。
strip.Items.Add(CreateCommandButton("打开", (sender, args) => OpenDocument())); // 添加打开文档命令。
strip.Items.Add(CreateCommandButton("保存", (sender, args) => SaveDocument(false))); // 添加保存文档命令。
strip.Items.Add(new ToolStripSeparator()); // 分隔文件和编辑命令。
select = CreateCommandButton("选择/移动", (sender, args) => SetCanvasMode(CanvasMode.Select)); // 添加选择模式命令。
connect = CreateCommandButton("连接节点", (sender, args) => SetCanvasMode(CanvasMode.Connect)); // 添加连线模式命令。
select.CheckOnClick = true; // 允许按钮显示持续选中状态。
connect.CheckOnClick = true; // 允许按钮显示持续选中状态。
strip.Items.Add(select); // 将选择模式按钮加入工具栏。
strip.Items.Add(connect); // 将连线模式按钮加入工具栏。
strip.Items.Add(new ToolStripSeparator()); // 分隔模式和删除命令。
strip.Items.Add(CreateCommandButton("删除", (sender, args) => canvas.DeleteSelection())); // 添加删除选中对象命令。
strip.Items.Add(new ToolStripSeparator()); // 分隔普通编辑与脚本命令。
strip.Items.Add(CreateCommandButton("编辑脚本", (sender, args) => EditSelectedScript())); // 添加脚本代码编辑命令。
strip.Items.Add(CreateCommandButton("运行脚本", async (sender, args) => await RunSelectedScriptAsync(null))); // 添加 Roslyn 脚本执行命令。
return strip; // 返回配置完成的工具栏。
} // 结束创建工具栏。
private static ToolStripButton CreateCommandButton(string text, EventHandler handler) // 创建带单击命令的文字按钮。
{ // 开始创建工具栏按钮。
ToolStripButton button = new ToolStripButton(text); // 使用命令名称创建按钮。
button.DisplayStyle = ToolStripItemDisplayStyle.Text; // 使用不依赖资源文件的文字样式。
button.AutoSize = true; // 根据中文名称自动调整宽度。
button.Click = handler; // 连接调用方提供的命令逻辑。
return button; // 返回配置完成的按钮。
} // 结束创建工具栏按钮。
private SplitContainer CreateWorkspace() // 创建三栏设计工作区。
{ // 开始创建工作区布局。
SplitContainer outer = new SplitContainer(); // 创建左侧工具箱与其余区域的分隔器。
outer.Dock = DockStyle.Fill; // 填满菜单工具栏以下的区域。
outer.FixedPanel = FixedPanel.Panel1; // 固定工具箱宽度避免缩放变形。
outer.SplitterDistance = 190; // 设置工具箱初始宽度。
outer.Panel1.Padding = new Padding(8); // 为工具列表提供边缘留白。
outer.Panel1.Controls.Add(CreateGroup("节点工具箱", toolbox)); // 将工具箱放入带标题容器。
SplitContainer inner = new SplitContainer(); // 创建画布与属性面板分隔器。
inner.Dock = DockStyle.Fill; // 填满右侧剩余区域。
inner.FixedPanel = FixedPanel.Panel2; // 固定属性面板宽度。
inner.SplitterDistance = 820; // 为画布预留主要工作空间。
inner.Panel1.Padding = new Padding(8); // 为画布提供边缘留白。
inner.Panel2.Padding = new Padding(8); // 为属性面板提供边缘留白。
inner.Panel1.Controls.Add(CreateGroup("流程画布", canvas)); // 将画布放入带标题容器。
inner.Panel2.Controls.Add(CreateGroup("属性编辑", propertyGrid)); // 将属性表放入带标题容器。
outer.Panel2.Controls.Add(inner); // 将内部分隔器加入外部右侧区域。
return outer; // 返回完整工作区布局。
} // 结束创建工作区布局。
private static GroupBox CreateGroup(string title, Control content) // 创建统一样式的标题容器。
{ // 开始创建标题容器。
GroupBox group = new GroupBox(); // 创建分组框控件。
group.Text = title; // 设置区域中文标题。
group.Dock = DockStyle.Fill; // 让分组框填满父级面板。
group.Padding = new Padding(8); // 避免内容紧贴边框。
group.Controls.Add(content); // 将主要内容加入分组框。
return group; // 返回配置完成的分组框。
} // 结束创建标题容器。
private MenuStrip CreateMenu() // 创建文件和编辑菜单。
{ // 开始创建主菜单。
MenuStrip menu = new MenuStrip(); // 创建菜单栏容器。
ToolStripMenuItem fileMenu = new ToolStripMenuItem("文件(&F)"); // 创建文件操作菜单。
fileMenu.DropDownItems.Add(CreateMenuItem("新建(&N)", Keys.Control | Keys.N, (sender, args) => NewDocument(true))); // 添加新建快捷命令。
fileMenu.DropDownItems.Add(CreateMenuItem("打开(&O)...", Keys.Control | Keys.O, (sender, args) => OpenDocument())); // 添加打开快捷命令。
fileMenu.DropDownItems.Add(CreateMenuItem("保存(&S)", Keys.Control | Keys.S, (sender, args) => SaveDocument(false))); // 添加保存快捷命令。
fileMenu.DropDownItems.Add(CreateMenuItem("另存为(&A)...", Keys.Control | Keys.Shift | Keys.S, (sender, args) => SaveDocument(true))); // 添加另存为快捷命令。
fileMenu.DropDownItems.Add(new ToolStripSeparator()); // 分隔文档和退出命令。
fileMenu.DropDownItems.Add(CreateMenuItem("退出(&X)", Keys.Alt | Keys.F4, (sender, args) => Close())); // 添加退出应用命令。
ToolStripMenuItem editMenu = new ToolStripMenuItem("编辑(&E)"); // 创建编辑操作菜单。
editMenu.DropDownItems.Add(CreateMenuItem("选择/移动", Keys.F2, (sender, args) => SetCanvasMode(CanvasMode.Select))); // 添加选择模式快捷命令。
editMenu.DropDownItems.Add(CreateMenuItem("连接节点", Keys.F3, (sender, args) => SetCanvasMode(CanvasMode.Connect))); // 添加连线模式快捷命令。
editMenu.DropDownItems.Add(CreateMenuItem("删除选中项", Keys.Delete, (sender, args) => canvas.DeleteSelection())); // 添加删除选中项快捷命令。
ToolStripMenuItem scriptMenu = new ToolStripMenuItem("脚本(&S)"); // 创建动态脚本操作菜单。
scriptMenu.DropDownItems.Add(CreateMenuItem("编辑选中脚本", Keys.Control | Keys.E, (sender, args) => EditSelectedScript())); // 添加脚本编辑快捷命令。
scriptMenu.DropDownItems.Add(CreateMenuItem("运行选中脚本", Keys.F5, async (sender, args) => await RunSelectedScriptAsync(null))); // 添加脚本运行快捷命令。
menu.Items.Add(fileMenu); // 将文件菜单加入菜单栏。
menu.Items.Add(editMenu); // 将编辑菜单加入菜单栏。
menu.Items.Add(scriptMenu); // 将脚本菜单加入菜单栏。
return menu; // 返回配置完成的菜单栏。
} // 结束创建主菜单。
private static ToolStripMenuItem CreateMenuItem(string text, Keys shortcut, EventHandler handler) // 创建标准菜单命令项。
{ // 开始创建菜单命令项。
ToolStripMenuItem item = new ToolStripMenuItem(text); // 使用显示名称创建菜单项。
item.ShortcutKeys = shortcut; // 设置菜单项的键盘快捷键。
item.Click = handler; // 连接菜单项对应命令。
return item; // 返回配置完成的菜单项。
} // 结束创建菜单命令项。
private void WireEvents() // 连接设计器控件之间的交互事件。
{ // 开始连接界面事件。
toolbox.ItemDrag = ToolboxItemDrag; // 支持从工具箱拖出节点类型。
canvas.SelectionChanged = CanvasSelectionChanged; // 同步画布选择到属性面板。
canvas.DocumentChanged = CanvasDocumentChanged; // 跟踪画布产生的文档修改。
propertyGrid.PropertyValueChanged = PropertyGridValueChanged; // 重绘属性编辑后的节点或连线。
FormClosing = MainFormClosing; // 关闭窗口前检查未保存内容。
KeyDown = MainFormKeyDown; // 支持取消当前连线操作。
} // 结束连接界面事件。
private void ToolboxItemDrag(object sender, ItemDragEventArgs e) // 从工具箱发起节点类型拖放。
{ // 开始处理工具项拖动。
ListViewItem item = e.Item as ListViewItem; // 获取正在拖动的工具项。
if (item == null || !(item.Tag is NodeKind)) // 忽略缺少节点类型的异常工具项。
{ // 开始处理无效工具项。
return; // 不发起拖放操作。
} // 结束处理无效工具项。
DataObject data = new DataObject(); // 创建拖放数据容器。
data.SetData(WorkflowCanvas.NodeDragFormat, item.Tag); // 使用画布约定格式保存节点类型。
toolbox.DoDragDrop(data, DragDropEffects.Copy); // 以复制语义启动拖放操作。
} // 结束处理工具项拖动。
private void CanvasSelectionChanged(object sender, EventArgs e) // 将画布选择同步到属性面板。
{ // 开始同步选择对象。
propertyGrid.SelectedObject = canvas.SelectedObject; // 显示节点或连线可编辑属性。
propertyGrid.Refresh(); // 立即刷新移动后的坐标等动态值。
} // 结束同步选择对象。
private void CanvasDocumentChanged(object sender, EventArgs e) // 响应画布产生的数据修改。
{ // 开始处理文档修改。
SetDirty(true); // 将当前文档标记为未保存。
} // 结束处理文档修改。
private void PropertyGridValueChanged(object sender, PropertyValueChangedEventArgs e) // 提交属性面板中的对象修改。
{ // 开始处理属性修改。
WorkflowNode node = canvas.SelectedObject as WorkflowNode; // 获取可能被修改的节点。
if (node != null) // 保证节点尺寸和位置保持可操作范围。
{ // 开始规范化节点布局值。
node.Width = Math.Max(60, node.Width); // 防止节点宽度过小。
node.Height = Math.Max(40, node.Height); // 防止节点高度过小。
node.X = Math.Max(0, node.X); // 防止节点移出画布左侧。
node.Y = Math.Max(0, node.Y); // 防止节点移出画布顶部。
propertyGrid.Refresh(); // 显示规范化后的属性值。
} // 结束规范化节点布局值。
canvas.Invalidate(); // 根据新属性重绘设计内容。
SetDirty(true); // 标记属性修改尚未保存。
} // 结束处理属性修改。
private void SetCanvasMode(CanvasMode mode) // 切换画布选择或连接模式。
{ // 开始切换画布模式。
canvas.Mode = mode; // 将模式传递给画布交互逻辑。
selectButton.Checked = mode == CanvasMode.Select; // 同步选择按钮的高亮状态。
connectButton.Checked = mode == CanvasMode.Connect; // 同步连线按钮的高亮状态。
statusLabel.Text = mode == CanvasMode.Select ? "选择模式:单击选择,拖动节点移动位置。" : "连接模式:依次单击起点节点和终点节点。"; // 显示当前模式操作说明。
} // 结束切换画布模式。
private void EditSelectedScript() // 打开当前 C# 脚本节点的代码编辑器。
{ // 开始编辑选中脚本。
WorkflowNode node = canvas.SelectedObject as WorkflowNode; // 获取画布当前选中的节点。
if (node == null || node.Kind != NodeKind.Script) // 仅允许编辑 C# 脚本类型节点。
{ // 开始处理无效选择。
MessageBox.Show(this, "请先选择一个 C# 脚本节点。", "脚本编辑", MessageBoxButtons.OK, MessageBoxIcon.Information); // 提示用户选择正确节点。
return; // 不打开空脚本编辑器。
} // 结束处理无效选择。
using (ScriptEditorForm editor = new ScriptEditorForm(node.Name, node.ScriptCode)) // 创建自动释放的脚本编辑窗口。
{ // 开始使用脚本编辑窗口。
editor.RunRequested = async (sender, args) => await RunSelectedScriptAsync(editor); // 支持在编辑器中直接运行未保存代码。
if (editor.ShowDialog(this) == DialogResult.OK) // 等待用户保存或取消修改。
{ // 开始保存脚本修改。
node.ScriptCode = editor.ScriptCode; // 将编辑后的代码写回节点模型。
propertyGrid.Refresh(); // 刷新属性面板中的脚本文本。
SetDirty(true); // 标记脚本修改尚未保存到流程文件。
} // 结束保存脚本修改。
} // 释放脚本编辑窗口资源。
} // 结束编辑选中脚本。
private async System.Threading.Tasks.Task RunSelectedScriptAsync(ScriptEditorForm editor) // 使用 Roslyn 执行当前脚本节点代码。
{ // 开始运行选中脚本。
WorkflowNode node = canvas.SelectedObject as WorkflowNode; // 获取画布当前选中的节点。
if (node == null || node.Kind != NodeKind.Script) // 仅允许运行 C# 脚本类型节点。
{ // 开始处理无效选择。
MessageBox.Show(this, "请先选择一个 C# 脚本节点。", "运行脚本", MessageBoxButtons.OK, MessageBoxIcon.Information); // 提示用户选择正确节点。
return; // 不调用 Roslyn 运行服务。
} // 结束处理无效选择。
string code = editor == null ? node.ScriptCode : editor.ScriptCode; // 优先运行编辑器中尚未保存的代码。
statusLabel.Text = "正在编译并执行脚本:" node.Name; // 显示当前动态执行状态。
ScriptExecutionResult result = await scriptRuntime.ExecuteAsync(node, canvas.Document, code); // 异步编译并运行脚本代码。
node.LastResult = result.Output; // 将执行结果保存到节点只读属性。
propertyGrid.Refresh(); // 在属性面板中显示最近执行结果。
statusLabel.Text = result.Success ? "脚本执行成功:" node.Name : "脚本执行失败:" node.Name; // 显示最终执行状态。
if (editor != null) // 编辑器内运行时直接写入下方输出区域。
{ // 开始更新编辑器输出。
editor.ShowOutput(result.Output, result.Success); // 显示编译诊断、日志和返回值。
} // 结束更新编辑器输出。
else // 工具栏或菜单运行时使用结果对话框。
{ // 开始显示独立结果窗口。
MessageBox.Show(this, string.IsNullOrWhiteSpace(result.Output) ? "执行成功,没有输出。" : result.Output, result.Success ? "脚本执行成功" : "脚本执行失败", MessageBoxButtons.OK, result.Success ? MessageBoxIcon.Information : MessageBoxIcon.Error); // 显示完整脚本结果。
} // 结束显示独立结果窗口。
canvas.Invalidate(); // 重绘可能被脚本修改的流程节点。
SetDirty(true); // 标记脚本可能已经修改流程文档。
} // 结束运行选中脚本。
private void NewDocument(bool confirmChanges) // 创建新的空白流程文档。
{ // 开始创建新文档。
if (confirmChanges && !ConfirmDiscardChanges()) // 在需要时先确认未保存修改。
{ // 开始处理用户取消新建。
return; // 保留当前文档不变。
} // 结束处理用户取消新建。
currentFile = null; // 新文档尚未关联磁盘路径。
canvas.Document = new WorkflowDocument(); // 向画布提供新的空白文档。
SetCanvasMode(CanvasMode.Select); // 返回默认选择和移动模式。
SetDirty(false); // 将新文档标记为已保存状态。
} // 结束创建新文档。
private void OpenDocument() // 从 JSON 文件加载流程文档。
{ // 开始执行打开文档操作。
if (!ConfirmDiscardChanges()) // 先处理当前文档未保存修改。
{ // 开始处理用户取消打开。
return; // 不显示文件选择窗口。
} // 结束处理用户取消打开。
using (OpenFileDialog dialog = new OpenFileDialog()) // 创建自动释放的打开文件窗口。
{ // 开始使用打开文件窗口。
dialog.Title = "打开工作流程"; // 设置窗口操作说明。
dialog.Filter = "工作流程文件 (*.workflow.json)|*.workflow.json|JSON 文件 (*.json)|*.json|所有文件 (*.*)|*.*"; // 限制并说明支持的文件类型。
if (dialog.ShowDialog(this) != DialogResult.OK) // 等待用户选定流程文件。
{ // 开始处理用户取消选择。
return; // 保留当前文档不变。
} // 结束处理用户取消选择。
try // 捕获文件格式和磁盘读取错误。
{ // 开始尝试读取流程文件。
string json = File.ReadAllText(dialog.FileName, Encoding.UTF8); // 使用 UTF-8 读取完整 JSON 内容。
WorkflowDocument loaded = serializer.Deserialize<WorkflowDocument>(json); // 将 JSON 转换为流程文档。
ValidateDocument(loaded); // 验证并规范化加载的数据结构。
canvas.Document = loaded; // 在画布上显示加载完成的流程。
currentFile = dialog.FileName; // 记住后续保存使用的文件路径。
SetCanvasMode(CanvasMode.Select); // 加载后回到安全的选择模式。
SetDirty(false); // 将成功加载的文档标记为未修改。
statusLabel.Text = "已打开:" currentFile; // 显示成功打开的文件位置。
} // 结束尝试读取流程文件。
catch (Exception exception) // 处理所有可预期的加载失败。
{ // 开始显示加载错误。
MessageBox.Show(this, "无法打开流程文件:\r\n" exception.Message, "打开失败", MessageBoxButtons.OK, MessageBoxIcon.Error); // 向用户说明失败原因。
} // 结束显示加载错误。
} // 释放打开文件窗口资源。
} // 结束执行打开文档操作。
private bool SaveDocument(bool saveAs) // 将当前流程文档保存为 JSON 文件。
{ // 开始执行保存文档操作。
string fileName = currentFile; // 默认使用当前文档关联路径。
if (saveAs || string.IsNullOrWhiteSpace(fileName)) // 首次保存或另存为时选择路径。
{ // 开始选择保存路径。
using (SaveFileDialog dialog = new SaveFileDialog()) // 创建自动释放的保存文件窗口。
{ // 开始使用保存文件窗口。
dialog.Title = "保存工作流程"; // 设置窗口操作说明。
dialog.Filter = "工作流程文件 (*.workflow.json)|*.workflow.json|JSON 文件 (*.json)|*.json"; // 提供标准保存文件类型。
dialog.DefaultExt = "workflow.json"; // 设置缺少扩展名时的默认后缀。
dialog.AddExtension = true; // 自动补充用户未输入的扩展名。
dialog.FileName = string.IsNullOrWhiteSpace(currentFile) ? "未命名流程.workflow.json" : Path.GetFileName(currentFile); // 提供易于识别的默认文件名。
if (dialog.ShowDialog(this) != DialogResult.OK) // 等待用户确认保存位置。
{ // 开始处理用户取消保存。
return false; // 告知调用方文档没有保存。
} // 结束处理用户取消保存。
fileName = dialog.FileName; // 使用用户选定的完整路径。
} // 释放保存文件窗口资源。
} // 结束选择保存路径。
try // 捕获磁盘写入和序列化错误。
{ // 开始尝试保存流程文件。
string json = serializer.Serialize(canvas.Document); // 将当前文档转换为 JSON 文本。
File.WriteAllText(fileName, json, new UTF8Encoding(false)); // 使用无 BOM UTF-8 写入流程文件。
currentFile = fileName; // 记住成功保存的磁盘路径。
SetDirty(false); // 清除未保存修改标记。
statusLabel.Text = "已保存:" currentFile; // 显示成功保存的文件位置。
return true; // 告知调用方保存成功。
} // 结束尝试保存流程文件。
catch (Exception exception) // 处理所有可预期的保存失败。
{ // 开始显示保存错误。
MessageBox.Show(this, "无法保存流程文件:\r\n" exception.Message, "保存失败", MessageBoxButtons.OK, MessageBoxIcon.Error); // 向用户说明失败原因。
return false; // 告知调用方保存没有完成。
} // 结束显示保存错误。
} // 结束执行保存文档操作。
private static void ValidateDocument(WorkflowDocument document) // 验证并修复反序列化后的文档结构。
{ // 开始验证流程文档。
if (document == null) // 空 JSON 或类型不匹配时无法继续加载。
{ // 开始处理空文档。
throw new InvalidDataException("文件中没有有效的工作流程数据。"); // 抛出便于用户理解的格式错误。
} // 结束处理空文档。
document.Title = document.Title ?? "未命名流程"; // 修复可能为空的文档标题。
document.Nodes = document.Nodes ?? new System.Collections.Generic.List<WorkflowNode>(); // 修复缺失的节点集合。
document.Connections = document.Connections ?? new System.Collections.Generic.List<WorkflowConnection>(); // 修复缺失的连线集合。
foreach (WorkflowNode node in document.Nodes) // 逐个规范化加载的节点。
{ // 开始规范化一个节点。
node.Id = string.IsNullOrWhiteSpace(node.Id) ? Guid.NewGuid().ToString("N") : node.Id; // 为缺少标识的节点补充唯一值。
node.Name = node.Name ?? string.Empty; // 修复可能为空的节点名称。
node.Description = node.Description ?? string.Empty; // 修复可能为空的节点说明。
node.ScriptCode = node.ScriptCode ?? string.Empty; // 修复旧版流程中缺失的脚本代码。
node.LastResult = node.LastResult ?? string.Empty; // 修复旧版流程中缺失的脚本结果。
node.ScriptCode = node.ScriptCode ?? string.Empty; // 修复旧版流程中缺失的脚本代码。
node.LastResult = node.LastResult ?? string.Empty; // 修复旧版流程中缺失的脚本结果。
node.Width = Math.Max(60, node.Width); // 保证加载节点具有可操作宽度。
node.Height = Math.Max(40, node.Height); // 保证加载节点具有可操作高度。
node.X = Math.Max(0, node.X); // 防止节点位于画布左侧之外。
node.Y = Math.Max(0, node.Y); // 防止节点位于画布顶部之外。
} // 结束规范化一个节点。
System.Collections.Generic.HashSet<string> nodeIds = new System.Collections.Generic.HashSet<string>(document.Nodes.Select(item => item.Id)); // 收集所有有效节点标识。
document.Connections.RemoveAll(item => item == null || !nodeIds.Contains(item.FromNodeId) || !nodeIds.Contains(item.ToNodeId) || item.FromNodeId == item.ToNodeId); // 删除无效或自连接关系。
foreach (WorkflowConnection connection in document.Connections) // 逐个规范化有效连线。
{ // 开始规范化一个连线。
connection.Id = string.IsNullOrWhiteSpace(connection.Id) ? Guid.NewGuid().ToString("N") : connection.Id; // 为缺少标识的连线补充唯一值。
connection.Name = connection.Name ?? string.Empty; // 修复可能为空的连线名称。
connection.Condition = connection.Condition ?? string.Empty; // 修复可能为空的流转条件。
} // 结束规范化一个连线。
} // 结束验证流程文档。
private bool ConfirmDiscardChanges() // 询问用户如何处理未保存修改。
{ // 开始检查未保存状态。
if (!dirty) // 没有修改时无需显示确认窗口。
{ // 开始处理干净文档。
return true; // 允许调用方继续操作。
} // 结束处理干净文档。
DialogResult result = MessageBox.Show(this, "当前流程尚未保存,是否先保存?", "未保存的更改", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning); // 询问保存、放弃或取消。
if (result == DialogResult.Cancel) // 用户选择取消当前操作。
{ // 开始处理取消选择。
return false; // 阻止新建、打开或关闭。
} // 结束处理取消选择。
if (result == DialogResult.Yes) // 用户希望先保存当前文档。
{ // 开始处理保存选择。
return SaveDocument(false); // 仅在保存成功时允许继续。
} // 结束处理保存选择。
return true; // 用户明确选择放弃修改。
} // 结束检查未保存状态。
private void SetDirty(bool value) // 更新未保存标记和窗口标题。
{ // 开始更新文档状态。
dirty = value; // 保存新的修改状态。
string name = string.IsNullOrWhiteSpace(currentFile) ? "未命名流程" : Path.GetFileName(currentFile); // 获取窗口中显示的文档名称。
Text = name (dirty ? " *" : string.Empty) " - 工作流程图设计器"; // 使用星号提示未保存修改。
} // 结束更新文档状态。
private void MainFormKeyDown(object sender, KeyEventArgs e) // 处理窗体级键盘操作。
{ // 开始处理键盘按键。
if (e.KeyCode == Keys.Escape) // Escape 用于取消未完成的连线。
{ // 开始处理取消按键。
canvas.ClearSelection(); // 清除连接起点和选择状态。
SetCanvasMode(CanvasMode.Select); // 返回默认选择模式。
e.Handled = true; // 防止按键继续传递给子控件。
} // 结束处理取消按键。
} // 结束处理键盘按键。
private void MainFormClosing(object sender, FormClosingEventArgs e) // 关闭应用前保护未保存数据。
{ // 开始处理窗体关闭。
if (!ConfirmDiscardChanges()) // 检查用户是否允许关闭应用。
{ // 开始处理取消关闭。
e.Cancel = true; // 保持主窗体和当前文档打开。
} // 结束处理取消关闭。
} // 结束处理窗体关闭。
} // 结束主窗体类型定义。
} // 结束主界面命名空间定义。
好例子网口号:伸出你的我的手 — 分享!
相关软件
小贴士
感谢您为本站写下的评论,您的评论对其它用户来说具有重要的参考价值,所以请认真填写。
- 类似“顶”、“沙发”之类没有营养的文字,对勤劳贡献的楼主来说是令人沮丧的反馈信息。
- 相信您也不想看到一排文字/表情墙,所以请不要反馈意义不大的重复字符,也请尽量不要纯表情的回复。
- 提问之前请再仔细看一遍楼主的说明,或许是您遗漏了。
- 请勿到处挖坑绊人、招贴广告。既占空间让人厌烦,又没人会搭理,于人于己都无利。
关于好例子网
本站旨在为广大IT学习爱好者提供一个非营利性互相学习交流分享平台。本站所有资源都可以被免费获取学习研究。本站资源来自网友分享,对搜索内容的合法性不具有预见性、识别性、控制性,仅供学习研究,请务必在下载后24小时内给予删除,不得用于其他任何用途,否则后果自负。基于互联网的特殊性,平台无法对用户传输的作品、信息、内容的权属或合法性、安全性、合规性、真实性、科学性、完整权、有效性等进行实质审查;无论平台是否已进行审查,用户均应自行承担因其传输的作品、信息、内容而可能或已经产生的侵权或权属纠纷等法律责任。本站所有资源不代表本站的观点或立场,基于网友分享,根据中国法律《信息网络传播权保护条例》第二十二与二十三条之规定,若资源存在侵权或相关问题请联系本站客服人员,点此联系我们。关于更多版权及免责申明参见 版权及免责申明


网友评论
我要评论