или .vdx), чтобы блок-схему можно было открыть и редактировать в Microsoft Visio.
Я обнаружил, что Bizagi Modeler может выполнить это преобразование вручную, но мне нужно автоматизировать процесс программно — в идеале с использованием C#.
Я пробовал реализовать это на C#, но сгенерированный мною файл .vdx выходит пусто.
Что мне нужно
Преобразовать XML BPMN (пример ниже) в редактируемый файл Visio
Сохранять точно тот же рабочий процесс и макет
В идеале добиться этого без использования Bizagi или другого графического интерфейса инструменты
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using Aspose.Diagram;
using Aspose.Diagram.Manipulation;
class Program
{
// Basic fallback mapping for stencil mode
static readonly Dictionary BasicShapeMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "startEvent", "Circle" },
{ "endEvent", "Circle" },
{ "exclusiveGateway", "Diamond" },
{ "parallelGateway", "Diamond" },
{ "task", "Rectangle" },
{ "userTask", "Rectangle" },
{ "serviceTask", "Rectangle" },
{ "participant", "Rectangle" } // pools as rectangles (fallback)
};
// Optional: if you also load a BPMN stencil, map BPMN tags to real BPMN masters here.
static readonly Dictionary BpmnStencilMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "startEvent", "Start Event" },
{ "endEvent", "End Event" },
{ "task", "Task" },
{ "userTask", "User Task" },
{ "serviceTask", "Service Task" },
{ "exclusiveGateway", "Gateway Exclusive" },
{ "parallelGateway", "Gateway Parallel" },
{ "participant", "Pool / Lane" } // actual name may vary by stencil
};
static int Main(string[] args)
{
try
{
var o = ParseArgs(args);
if (o == null)
{
PrintHelp();
return 1;
}
if (o.Mode.Equals("svg", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(o.SvgPath) || string.IsNullOrWhiteSpace(o.OutPath))
{
Console.WriteLine("Missing --svg or --out for svg mode.");
return 1;
}
SvgToVisio(o.SvgPath, o.OutPath);
Console.WriteLine($"✅ SVG embedded → {o.OutPath}");
return 0;
}
// Stencil mode
if (string.IsNullOrWhiteSpace(o.BpmnPath) || string.IsNullOrWhiteSpace(o.OutPath) || string.IsNullOrWhiteSpace(o.StencilPath))
{
Console.WriteLine("Missing --bpmn, --out, or --stencil for stencil mode.");
return 1;
}
Console.WriteLine("[1/2] Parsing BPMN…");
var (nodes, edges, shapes, flows) = ParseBpmn(o.BpmnPath!);
Console.WriteLine("[2/2] Building Visio (stencil)...");
BpmnToVisioWithStencil(
basicStencilPath: o.StencilPath!,
bpmnStencilPath: o.BpmnStencilPath,
nodes: nodes,
edges: edges,
shapes: shapes,
flows: flows,
outPath: o.OutPath!
);
Console.WriteLine($"✅ Done → {o.OutPath}");
return 0;
}
catch (Exception ex)
{
Console.WriteLine("❌ Error: " + ex);
return 2;
}
}
// =========================
// MODE 2: SVG → Visio page
// =========================
static void SvgToVisio(string svgPath, string outPath)
{
if (!File.Exists(svgPath))
throw new FileNotFoundException("SVG file not found", svgPath);
var d = new Diagram();
var page = d.Pages[0];
// Place the SVG as a vector object on the page.
// Aspose.Diagram 25.10.0 supports AddShape with a stream for images (including SVG).
using (var fs = File.OpenRead(svgPath))
{
double x = 1, y = 1, w = 800, h = 600; // adjust as needed; or parse viewBox from SVG
long sid = page.AddShape(x, y, w, h, fs);
var shp = page.Shapes.GetShape(sid);
shp.Name = "BPMN_SVG";
}
d.Save(outPath, SaveFileFormat.Vsdx);
}
// ==========================================
// MODE 1: BPMN + Stencil (editable shapes)
// ==========================================
static void BpmnToVisioWithStencil(
string basicStencilPath,
string? bpmnStencilPath,
Dictionary nodes,
List edges,
Dictionary shapes,
Dictionary flows,
string outPath)
{
if (!File.Exists(basicStencilPath))
throw new FileNotFoundException("Basic stencil not found", basicStencilPath);
// Load a diagram from the basic stencil so its masters exist (Rectangle/Circle/Diamond/Dynamic connector).
var d = new Diagram(basicStencilPath);
// Optionally merge a BPMN stencil (masters copied into current diagram)
if (!string.IsNullOrWhiteSpace(bpmnStencilPath))
{
if (!File.Exists(bpmnStencilPath))
throw new FileNotFoundException("BPMN stencil not found", bpmnStencilPath);
var bpmn = new Diagram(bpmnStencilPath);
CopyMastersIfMissing(bpmn, d);
}
var page = d.Pages[0];
var idToShape = new Dictionary();
// Resolve a usable master name for a BPMN type.
string ResolveMasterName(string bpmnType)
{
// Prefer BPMN stencil name if present in diagram masters
if (BpmnStencilMap.TryGetValue(bpmnType, out var bpmnName) &&
TryFindMaster(d, bpmnName, out var foundBpmn))
{
return foundBpmn;
}
// Fallback to basic shape master
if (BasicShapeMap.TryGetValue(bpmnType, out var basicName) &&
TryFindMaster(d, basicName, out var foundBasic))
{
return foundBasic;
}
// Fallback to Rectangle if available
if (TryFindMaster(d, "Rectangle", out var rect))
return rect;
// Last resort: any master present
foreach (Master m in d.Masters)
return m.Name;
throw new DiagramException("No masters available in loaded stencil(s).");
}
// Add node shapes
foreach (var kv in nodes)
{
string id = kv.Key;
var (name, type) = kv.Value;
if (!shapes.TryGetValue(id, out var box)) continue;
var (x, y, w, h) = box;
string masterName = ResolveMasterName(type);
long sid = page.AddShape(x, y, w, h, masterName); // IMPORTANT: string master name (Aspose 25.10.0)
var shp = page.Shapes.GetShape(sid);
shp.Text.Value.Add(new Txt(string.IsNullOrWhiteSpace(name) ? type : name));
idToShape[id] = shp;
}
// Ensure we can create connectors
string dynName = TryFindMaster(d, "Dynamic connector", out var dyn) ? dyn
: throw new DiagramException("Dynamic connector master not found in the loaded stencil(s).");
// Add connectors with DI waypoints
foreach (var (eid, src, tgt) in edges)
{
if (!idToShape.TryGetValue(src, out var sShape)) continue;
if (!idToShape.TryGetValue(tgt, out var tShape)) continue;
long connId = page.AddShape(0, 0, 0, 0, dynName);
var connector = page.Shapes.GetShape(connId);
if (flows.TryGetValue(eid, out var pts) && pts.Count > 0)
{
foreach (var (px, py) in pts)
{
var c = new Connection();
c.X.Value = px;
c.Y.Value = py;
connector.Connections.Add(c);
}
}
// Connect using shapes & enum (Aspose 25.10.0)
page.ConnectShapesViaConnector(
sShape, ConnectionPointPlace.Right,
tShape, ConnectionPointPlace.Left,
connector
);
}
d.Save(outPath, SaveFileFormat.Vsdx);
}
// Copy masters from 'src' into 'dst' if name missing
static void CopyMastersIfMissing(Diagram src, Diagram dst)
{
foreach (Master m in src.Masters)
{
if (!TryFindMaster(dst, m.Name, out _))
{
dst.Masters.Add(m);
}
}
}
// Try to find a master by name in current diagram (Aspose 25.10.0 requires manual scan).
static bool TryFindMaster(Diagram d, string name, out string actualName)
{
foreach (Master m in d.Masters)
{
if (string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase))
{
actualName = m.Name;
return true;
}
}
actualName = null!;
return false;
}
// =========================
// BPMN parse with DI
// =========================
static (Dictionary,
List,
Dictionary,
Dictionary)
ParseBpmn(string file)
{
var doc = new XmlDocument();
doc.Load(file);
var ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("bpmn", "http://www.omg.org/spec/BPMN/20100524/MODEL");
ns.AddNamespace("bpmndi", "http://www.omg.org/spec/BPMN/20100524/DI");
ns.AddNamespace("dc", "http://www.omg.org/spec/DD/20100524/DC");
ns.AddNamespace("di", "http://www.omg.org/spec/DD/20100524/DI");
var nodes = new Dictionary();
var edges = new List();
var shapes = new Dictionary();
var flows = new Dictionary();
foreach (XmlNode s in doc.SelectNodes("//bpmndi:BPMNShape", ns)!)
{
string bpmnId = s.Attributes!["bpmnElement"]!.Value;
var b = s.SelectSingleNode("dc:Bounds", ns)!;
double x = double.Parse(b.Attributes!["x"]!.Value, CultureInfo.InvariantCulture);
double y = double.Parse(b.Attributes!["y"]!.Value, CultureInfo.InvariantCulture);
double w = double.Parse(b.Attributes!["width"]!.Value, CultureInfo.InvariantCulture);
double h = double.Parse(b.Attributes!["height"]!.Value, CultureInfo.InvariantCulture);
shapes[bpmnId] = (x, y, w, h);
}
foreach (XmlNode e in doc.SelectNodes("//bpmndi:BPMNEdge", ns)!)
{
string bpmnId = e.Attributes!["bpmnElement"]!.Value;
var pts = new List();
foreach (XmlNode wp in e.SelectNodes("di:waypoint", ns)!)
{
double x = double.Parse(wp.Attributes!["x"]!.Value, CultureInfo.InvariantCulture);
double y = double.Parse(wp.Attributes!["y"]!.Value, CultureInfo.InvariantCulture);
pts.Add((x, y));
}
flows[bpmnId] = pts;
}
foreach (XmlNode n in doc.SelectNodes("//bpmn:*", ns)!)
{
string tag = n.LocalName;
if (BasicShapeMap.ContainsKey(tag) || tag.Contains("task", StringComparison.OrdinalIgnoreCase))
{
string id = n.Attributes!["id"]!.Value;
string? name = n.Attributes!["name"]?.Value?.Trim();
if (string.IsNullOrEmpty(name)) name = tag;
nodes[id] = (name!, tag);
}
if (tag == "sequenceFlow")
{
edges.Add((
n.Attributes!["id"]!.Value,
n.Attributes!["sourceRef"]!.Value,
n.Attributes!["targetRef"]!.Value
));
}
}
return (nodes, edges, shapes, flows);
}
// =========================
// CLI parsing & help
// =========================
class Options
{
public string Mode = "stencil"; // stencil | svg
public string? BpmnPath;
public string? OutPath;
public string? StencilPath; // REQUIRED for stencil mode
public string? BpmnStencilPath; // OPTIONAL for real BPMN masters
public string? SvgPath; // for svg mode
}
static Options? ParseArgs(string[] args)
{
if (args.Length == 0) return null;
var o = new Options();
for (int i = 0; i < args.Length; i++)
{
string a = args[i];
if (a == "--mode" && i + 1 < args.Length) o.Mode = args[++i];
else if (a == "--bpmn" && i + 1 < args.Length) o.BpmnPath = args[++i];
else if (a == "--out" && i + 1 < args.Length) o.OutPath = args[++i];
else if (a == "--stencil" && i + 1 < args.Length) o.StencilPath = args[++i];
else if (a == "--bpmnStencil" && i + 1 < args.Length) o.BpmnStencilPath = args[++i];
else if (a == "--svg" && i + 1 < args.Length) o.SvgPath = args[++i];
}
return o;
}
static void PrintHelp()
{
Console.WriteLine(@"
Bpmn2Visio (Aspose.Diagram 25.10.0)
USAGE:
# Mode 1 — Stencil (editable shapes)
Bpmn2Visio.exe --mode stencil --bpmn input.bpmn --out output.vsdx --stencil ""C:\...\BASIC_U.VSSX"" [--bpmnStencil ""C:\...\BPMN.vssx""]
# Mode 2 — SVG (pixel-perfect)
Bpmn2Visio.exe --mode svg --svg diagram.svg --out output.vsdx
NOTES:
- In Stencil mode you MUST pass a stencil that includes 'Rectangle', 'Circle', 'Diamond', and 'Dynamic connector' (e.g., BASIC_U.VSSX).
- Optionally add a BPMN stencil to get true BPMN icons.
- In SVG mode, export SVG from Camunda Modeler (File → Export → SVG) and pass it via --svg. The SVG is embedded as a single vector object.
");
}
}
Однако созданный файл .vsdx открывается в Visio но совершенно пуст — не отображаются никакие фигуры или соединители.
Теперь мой вопрос:
Как правильно преобразовать XML-файл BPMN в редактируемую диаграмму Visio программным способом на C#?
Цель: создать Файл Visio, который выглядит и ведет себя так же, как экспорт BPMN Bizagi — редактируемый и правильно отформатированный.
Необходима помощь, как и каким путем я могу достичь желаемой цели.
Я создал [b]файл BPMN из XML[/b] и теперь хочу [b]преобразовать его в файл Visio[/b] ([code].vsdx[/code] или .vdx), чтобы блок-схему можно было открыть и [b]редактировать в Microsoft Visio[/b]. Я обнаружил, что Bizagi Modeler может выполнить это преобразование вручную, но мне нужно автоматизировать процесс программно — в идеале с использованием C#. Я пробовал реализовать это на C#, но сгенерированный мною файл .vdx выходит [b]пусто[/b].
🧩 Что мне нужно [list] [*]Преобразовать XML BPMN (пример ниже) в редактируемый файл Visio [*]Сохранять [b]точно тот же рабочий процесс и макет[/b] [*]В идеале добиться этого без использования Bizagi или другого графического интерфейса инструменты [/list] [code]
[img]https://i.sstatic.net/Wi82yU8w.png[/img] Код, который я пробовал (C# с использованием Aspose.Diagram) [code]using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Xml; using Aspose.Diagram; using Aspose.Diagram.Manipulation;
// ========================= // MODE 2: SVG → Visio page // ========================= static void SvgToVisio(string svgPath, string outPath) { if (!File.Exists(svgPath)) throw new FileNotFoundException("SVG file not found", svgPath);
var d = new Diagram(); var page = d.Pages[0];
// Place the SVG as a vector object on the page. // Aspose.Diagram 25.10.0 supports AddShape with a stream for images (including SVG). using (var fs = File.OpenRead(svgPath)) { double x = 1, y = 1, w = 800, h = 600; // adjust as needed; or parse viewBox from SVG long sid = page.AddShape(x, y, w, h, fs); var shp = page.Shapes.GetShape(sid); shp.Name = "BPMN_SVG"; }
d.Save(outPath, SaveFileFormat.Vsdx); }
// ========================================== // MODE 1: BPMN + Stencil (editable shapes) // ========================================== static void BpmnToVisioWithStencil( string basicStencilPath, string? bpmnStencilPath, Dictionary nodes, List edges, Dictionary shapes, Dictionary flows, string outPath) { if (!File.Exists(basicStencilPath)) throw new FileNotFoundException("Basic stencil not found", basicStencilPath);
// Load a diagram from the basic stencil so its masters exist (Rectangle/Circle/Diamond/Dynamic connector). var d = new Diagram(basicStencilPath);
// Optionally merge a BPMN stencil (masters copied into current diagram) if (!string.IsNullOrWhiteSpace(bpmnStencilPath)) { if (!File.Exists(bpmnStencilPath)) throw new FileNotFoundException("BPMN stencil not found", bpmnStencilPath);
var bpmn = new Diagram(bpmnStencilPath); CopyMastersIfMissing(bpmn, d); }
var page = d.Pages[0]; var idToShape = new Dictionary();
// Resolve a usable master name for a BPMN type. string ResolveMasterName(string bpmnType) { // Prefer BPMN stencil name if present in diagram masters if (BpmnStencilMap.TryGetValue(bpmnType, out var bpmnName) && TryFindMaster(d, bpmnName, out var foundBpmn)) { return foundBpmn; }
// Fallback to basic shape master if (BasicShapeMap.TryGetValue(bpmnType, out var basicName) && TryFindMaster(d, basicName, out var foundBasic)) { return foundBasic; }
// Fallback to Rectangle if available if (TryFindMaster(d, "Rectangle", out var rect)) return rect;
// Last resort: any master present foreach (Master m in d.Masters) return m.Name;
throw new DiagramException("No masters available in loaded stencil(s)."); }
// Add node shapes foreach (var kv in nodes) { string id = kv.Key; var (name, type) = kv.Value; if (!shapes.TryGetValue(id, out var box)) continue;
var (x, y, w, h) = box; string masterName = ResolveMasterName(type);
long sid = page.AddShape(x, y, w, h, masterName); // IMPORTANT: string master name (Aspose 25.10.0) var shp = page.Shapes.GetShape(sid); shp.Text.Value.Add(new Txt(string.IsNullOrWhiteSpace(name) ? type : name)); idToShape[id] = shp; }
// Ensure we can create connectors string dynName = TryFindMaster(d, "Dynamic connector", out var dyn) ? dyn : throw new DiagramException("Dynamic connector master not found in the loaded stencil(s).");
// Add connectors with DI waypoints foreach (var (eid, src, tgt) in edges) { if (!idToShape.TryGetValue(src, out var sShape)) continue; if (!idToShape.TryGetValue(tgt, out var tShape)) continue;
long connId = page.AddShape(0, 0, 0, 0, dynName); var connector = page.Shapes.GetShape(connId);
if (flows.TryGetValue(eid, out var pts) && pts.Count > 0) { foreach (var (px, py) in pts) { var c = new Connection(); c.X.Value = px; c.Y.Value = py; connector.Connections.Add(c); } }
// Copy masters from 'src' into 'dst' if name missing static void CopyMastersIfMissing(Diagram src, Diagram dst) { foreach (Master m in src.Masters) { if (!TryFindMaster(dst, m.Name, out _)) { dst.Masters.Add(m); } } }
// Try to find a master by name in current diagram (Aspose 25.10.0 requires manual scan). static bool TryFindMaster(Diagram d, string name, out string actualName) { foreach (Master m in d.Masters) { if (string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)) { actualName = m.Name; return true; } } actualName = null!; return false; }
// ========================= // BPMN parse with DI // ========================= static (Dictionary, List, Dictionary, Dictionary) ParseBpmn(string file) { var doc = new XmlDocument(); doc.Load(file);
var ns = new XmlNamespaceManager(doc.NameTable); ns.AddNamespace("bpmn", "http://www.omg.org/spec/BPMN/20100524/MODEL"); ns.AddNamespace("bpmndi", "http://www.omg.org/spec/BPMN/20100524/DI"); ns.AddNamespace("dc", "http://www.omg.org/spec/DD/20100524/DC"); ns.AddNamespace("di", "http://www.omg.org/spec/DD/20100524/DI");
var nodes = new Dictionary(); var edges = new List(); var shapes = new Dictionary(); var flows = new Dictionary();
foreach (XmlNode s in doc.SelectNodes("//bpmndi:BPMNShape", ns)!) { string bpmnId = s.Attributes!["bpmnElement"]!.Value; var b = s.SelectSingleNode("dc:Bounds", ns)!;
double x = double.Parse(b.Attributes!["x"]!.Value, CultureInfo.InvariantCulture); double y = double.Parse(b.Attributes!["y"]!.Value, CultureInfo.InvariantCulture); double w = double.Parse(b.Attributes!["width"]!.Value, CultureInfo.InvariantCulture); double h = double.Parse(b.Attributes!["height"]!.Value, CultureInfo.InvariantCulture);
shapes[bpmnId] = (x, y, w, h); }
foreach (XmlNode e in doc.SelectNodes("//bpmndi:BPMNEdge", ns)!) { string bpmnId = e.Attributes!["bpmnElement"]!.Value; var pts = new List(); foreach (XmlNode wp in e.SelectNodes("di:waypoint", ns)!) { double x = double.Parse(wp.Attributes!["x"]!.Value, CultureInfo.InvariantCulture); double y = double.Parse(wp.Attributes!["y"]!.Value, CultureInfo.InvariantCulture); pts.Add((x, y)); } flows[bpmnId] = pts; }
foreach (XmlNode n in doc.SelectNodes("//bpmn:*", ns)!) { string tag = n.LocalName;
if (BasicShapeMap.ContainsKey(tag) || tag.Contains("task", StringComparison.OrdinalIgnoreCase)) { string id = n.Attributes!["id"]!.Value; string? name = n.Attributes!["name"]?.Value?.Trim(); if (string.IsNullOrEmpty(name)) name = tag; nodes[id] = (name!, tag); }
// ========================= // CLI parsing & help // ========================= class Options { public string Mode = "stencil"; // stencil | svg public string? BpmnPath; public string? OutPath; public string? StencilPath; // REQUIRED for stencil mode public string? BpmnStencilPath; // OPTIONAL for real BPMN masters public string? SvgPath; // for svg mode }
static Options? ParseArgs(string[] args) { if (args.Length == 0) return null; var o = new Options();
for (int i = 0; i < args.Length; i++) { string a = args[i]; if (a == "--mode" && i + 1 < args.Length) o.Mode = args[++i]; else if (a == "--bpmn" && i + 1 < args.Length) o.BpmnPath = args[++i]; else if (a == "--out" && i + 1 < args.Length) o.OutPath = args[++i]; else if (a == "--stencil" && i + 1 < args.Length) o.StencilPath = args[++i]; else if (a == "--bpmnStencil" && i + 1 < args.Length) o.BpmnStencilPath = args[++i]; else if (a == "--svg" && i + 1 < args.Length) o.SvgPath = args[++i]; }
NOTES: - In Stencil mode you MUST pass a stencil that includes 'Rectangle', 'Circle', 'Diamond', and 'Dynamic connector' (e.g., BASIC_U.VSSX). - Optionally add a BPMN stencil to get true BPMN icons. - In SVG mode, export SVG from Camunda Modeler (File → Export → SVG) and pass it via --svg. The SVG is embedded as a single vector object. "); } } [/code] Однако созданный файл .vsdx открывается в Visio [b]но совершенно пуст[/b] — не отображаются никакие фигуры или соединители. Теперь мой вопрос:
Как правильно преобразовать XML-файл BPMN в [b]редактируемую диаграмму Visio[/b] программным способом на C#? Цель: создать Файл Visio, который выглядит и ведет себя так же, как экспорт BPMN Bizagi — редактируемый и правильно отформатированный. Необходима помощь, как и каким путем я могу достичь желаемой цели.