cad.net Acad外部启动程序
故事
首先是飞诗在问了一个问题:
Acad2007的com包围盒无法正确获取文字的包围盒,问有没有其他方法?
他测试在Lisp的获取是正确的,所以他想反射调用里面内置的.
而他会反射,但是获取不到在参数传回返回值.edata解决了这个问题,
他在《精通.NET互操作:P/Invoke,C++ Interop和COM.Interop》 黄际洲 崔晓源 编著
264页中找到一段从参数返回结果的.
然后我测试的时候,发现net5桌面程序不能用:Marshal.GetActiveObject真是一个问题接一个.
虽然我在学net5的教学中知道了这个,但是它没有给我解决方案.
然后南胜提醒了我一下,有using Microsoft.VisualBasic.Interaction.CreateObject可以用.
为此我完成了之前没有完成的东西:com启动Acad
在测试期间我还发现Acad08启动之后获取com对象,
会获取到17.1和17两个对象,因此才知道了我之前做的com发送命令为什么发送了两次.
说明
com无法在同cad版本进行发送操作,它只会执行一次同版本,而不同版本则可以各执行一次.(这里可能带来错误,详情本文测试VB通过文档名获取Com实例
)
为了解决这个问题,
还可以利用进程句柄发送消息
以及跨进程通讯Remoting,
网络通讯等等..
所以com用来启动cad还是蛮好的.
启动cad的各种方式
1,注册表获取acad.exe路径启动
2,com版本号启动,指定精确版本:"AutoCAD.Application.17.1".
3,com版本号启动,指定大版本号,启动的可能是17系列的,但是2007必然是这个啊:"AutoCAD.Application.17"
4,com不指定版本号,启动最后一次的启动:"AutoCAD.Application".
5,根据guid启动,可以找注册表上面对应的exe,也可以像下面一样找com版本号
上述如果是管理员,那么必须要精确版本.
32位程序无权读取64位程序的信息
读取注册表时候注意一下anyCPU/x86/x64
代码
写一个net5的控制台调用这个,当然了,你喜欢低版本也可以...
[STAThread]
static void Main() {
try
{
// 有启动中的Acad就直接利用否则新开,然后获取包围盒
var acad = new AcadProcess();
acad.GetBoundingBox();
// 新开Acad
// acad.Run(RunCadVer.Minimum);
// acad.Run(RunCadVer.Maximum);
var process = acad.ProcessArray;
Debug.WriteLine($"当前启动了 {process.Length} 个Acad");
foreach (var item in process)
Debug.WriteLine($"进程id是:{item.Id}");
}
catch (Exception ex)
{
throw new Exception("无法创建或附加到AutoCAD对象: " + ex.Message);
}
}
AcadProcess.cs 指定版本启动cad
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
namespace JoinBox.BasalRegistry;
public enum RunCadVer {
Minimum,
Maximum,
All,
}
public class AcadProcess {
string ProgID = "AutoCAD.Application";
const string _acad = "acad";
SortedSet<AcadClsIdInfo> _acadClsIdInfos;
/// <summary>
/// 已经启动的Acad进程
/// </summary>
public Process[] ProcessArray { get => Process.GetProcessesByName(_acad); }
public AcadProcess() {
_acadClsIdInfos = new();
//管理员模式必须要完整的版本号
if (ProcessHelper.IsAdministrator()) {
var ks = AcadRegedit.AcadProductKeys;
if (ks.Count != 0)
ProgID += "." + ks[ks.Count - 1].VersionToString();
}
}
/// <summary>
/// 这里获取所有已经记录GUID的cad,没有记录将会由启动最新的时候加入
/// </summary>
void GetAcadComs()
{
AcadClsIdHelper.GetActiveAcadCom(_acadClsIdInfos);
#if DEBUG2
_acadClsIdInfos.Add(new("16.0"));
_acadClsIdInfos.Add(new("16.1"));
_acadClsIdInfos.Add(new("16.2"));
_acadClsIdInfos.Add(new("15.1"));
_acadClsIdInfos.Add(new("18.2"));
_acadClsIdInfos.Add(new("17.5"));
_acadClsIdInfos.Add(new("12.5"));
_acadClsIdInfos.Add(new("12.4"));
_acadClsIdInfos.Add(new("12.0"));
#endif
if (_acadClsIdInfos.Count <2) //启动cad的时候会是1,减少运算
return;
// 开启了Acad08,那么它是17.1,此时17也会存在,
// 所以会有两个obj被保存,需要避免发送两次命令到同一个cad内,
// 因此,此处需要过滤同系列号的,举出最高版本.
// 排序17.2>17.1>17>16.1>16,将17.2保留,删除17.1>17,保留16.1,删除16
for (int i = 0; i < _acadClsIdInfos.Count; i++)
{
var dete = _acadClsIdInfos[i].VerNumber - (int)_acadClsIdInfos[i].VerNumber;//求小数部分
for (int j = i + 1; j < _acadClsIdInfos.Count; j++)
{
if (_acadClsIdInfos[i].VerNumber - _acadClsIdInfos[j].VerNumber <= dete)
{
_acadClsIdInfos.Remove(_acadClsIdInfos[j]);
j--;
}
else
break;
}
}
}
}
/// <summary>
/// 启动Acad程序,指定版本
/// </summary>
/// <param name="runCadVer">启动什么版本</param>
public void Run(RunCadVer runCadVer = RunCadVer.Minimum)
{
const string _acadexe = "\\acad.exe";
string progID = "AutoCAD.Application.";
//获取本机cad路径
var apks = AcadRegedit.AcadProductKeys;
if (apks.Count == 0)
return;
string exePath;
switch (runCadVer)
{
case RunCadVer.Minimum:
{
var apk = apks[0];
progID += apk.VersionToString();
exePath = apk.Location + _acadexe;
var acadApp = StartApplicat.Run(progID, exePath);
_acadClsIdInfos.Add(new AcadClsIdInfo(new Guid(), acadApp, apk));
}
break;
case RunCadVer.Maximum:
{
var apk = apks[apks.Count - 1];
progID += apk.VersionToString();
exePath = apk.Location + _acadexe;
var acadApp = StartApplicat.Run(progID, exePath);
_acadClsIdInfos.Add(new AcadClsIdInfo(new Guid(), acadApp, apk));
}
break;
case RunCadVer.All:
{
foreach (var apk in apks)
{
var acadApp = StartApplicat.Run(progID + apk.VersionToString(), apk.Location + _acadexe);
exePath = apk.Location + _acadexe;
_acadClsIdInfos.Add(new AcadClsIdInfo(new Guid(), acadApp, apk));
}
}
break;
}
}
/// <summary>
/// 通过cad的com进行反射调用VBA函数获取包围盒
/// </summary>
public void GetBoundingBox()
{
//使用 _acadClsIdInfos.Add 将导致重复获取了com和启动时的所以消重/但是我换结构了
GetAcadComs();//遍历当前
if (_acadClsIdInfos.Count == 0)
{
Run();
//GetAcadComs();//可以看到Run加入和ClsId加入的区别,用来测试消重是否正确
}
var comObject = Type.GetTypeFromProgID(ProgID);
if (comObject == null)
throw new ArgumentNullException($"本机不存在:{ProgID}");
foreach (var acadClsIdInfo in _acadClsIdInfos)
{
var acadAppCom = acadClsIdInfo.Com;
#if true
//参数
object[] args = new object[1];
//设置需要设置的参数值
args[0] = true;
//设置属性-可视,显示窗体
comObject.InvokeMember("Visible", BindingFlags.SetProperty, null, acadAppCom, args);
//获取属性
object comAcDoc = comObject.InvokeMember("ActiveDocument", BindingFlags.GetProperty, null, acadAppCom, null);
object comAcMsSpace = comObject.InvokeMember("ModelSpace", BindingFlags.GetProperty, null, comAcDoc, null);
//调用VBA函数也就是com暴露的函数,画在"激活的文档"的"模型空间"然后输入画一条线的坐标数据.
object[] lines = new object[] { new double[] { 100, 100, 0 }, new double[] { 300, 300, 0 } };
object comAcLine = comObject.InvokeMember("AddLine", BindingFlags.InvokeMethod, null, comAcMsSpace, lines);
//pts就是包围盒返回的点集
object[] pts = new object[2] { null, null };
//由于需要从参数中返回结果,所以需要设置 ParameterModifier 作用在 InvokeMember 上
var paramMod = new ParameterModifier(2);
paramMod[0] = true;//设置为true才能改写
paramMod[1] = true;
//求得这条线的包围盒,返回给pts.
comObject.InvokeMember("GetBoundingBox",
BindingFlags.SuppressChangeType | BindingFlags.InvokeMethod,
null, comAcLine, pts, new ParameterModifier[] { paramMod }, null, null);
//全屏显示
comObject.InvokeMember("ZoomAll", BindingFlags.InvokeMethod, null, acadAppCom, null);
#else
//c#4等效代码
acadAppCom.ZoomAll();
dynamic acadDoc = acadAppCom.ActiveDocument;
if (acadDoc != null)
{
//acadDoc.SendCommand("_.Line 100,100 300,300 ");
dynamic acMsSpace = acadDoc.ModelSpace;
double[] p1 = new double[3] { 100.0, 100.0, 0.0 };
double[] p2 = new double[3] { 300.0, 300.0, 0.0 };
dynamic acLine = acMsSpace.AddLine(p1, p2);
object ptMin = new object();
object ptMax = new object();
acLine.GetBoundingBox(out ptMin, out ptMax);
MessageBox.Show(ptMin.ToString() + "\n" + ptMax.ToString());
}
#endif
var a = (double[])pts[0];
var b = (double[])pts[1];
Debug.WriteLine($"MinPoint={a[0]},{a[1]},{a[2]}");
Debug.WriteLine($"MaxPoint={b[0]},{b[1]},{b[2]}");
}
}
#endregion
}
AcadRegedit.cs 注册表
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Win32;
namespace JoinBox.BasalRegistry {
public class AcadProductKey {
/// <summary>
/// 注册表位置
/// </summary>
public RegistryKey ProductKey;
/// <summary>
/// cad安装路径
/// </summary>
public string Location;
/// <summary>
/// Acad名称
/// </summary>
public string ProductName;
/// <summary>
/// cad版本号 17.1,这里用:大版本<<16 | 小版本,参考读取DWG
/// </summary>
public int Version {get;}
public void SetVersion(string str){
Version = GetHashCode(str);
}
public static int GetHashCode(string str){
var major = (s[1] - '0')*10 + (s[2] - '0');
var minor = (s[4] - '0'); //0是R,3是小数点,所以跳到4
return (major << 16) | minor;
}
public string VersionToString(){
// 解码版本号
var major = Version >> 16;
var minor = Version & 0xFFFF;//位与掩码,获取低16bit
return major.ToString()+"."+minor.ToString();
}
/// <summary>
/// 全球名唯一键
/// </summary>
public Guid Guid;
}
internal class AcadRegedit
{
/// <summary>
/// 注册表路径,已经最小到最大排序
/// </summary>
public static SortedSet<AcadProductKey> AcadProductKeys { get=>_apk; }
static AcadRegedit() {
GetAcadProductKeys()
GetGuidKeys();
}
/// <summary>
/// 获取Guid
/// </summary>
static void GetGuidKeys()
{
const string appReg = "AutoCAD.Application";
const string appRegDot = appReg + ".";
//var fmts = new string[] { @"Software\Classes", @"Software\Wow6432Node\Classes" };
var fmts = new string[] { @"Software\Classes" };//只需要在32位上面找
foreach (var fmt in fmts)
{
var classes = Registry.LocalMachine.OpenSubKey(fmt);
if (classes == null)
continue;
foreach (string verReg in classes.GetSubKeyNames())
{
try
{
if (!verReg.Contains(appReg)) continue;
var app = classes.OpenSubKey(verReg);
if (app == null) continue;
var clsid = app.OpenSubKey("CLSID");
if (clsid == null) continue;
var verStr = verReg.Replace(appRegDot, string.Empty);
if(_apk.TryGetValue(AcadProductKey.GetHashCode(verStr),out T k)) {
k.Guid = new Guid(clsid.GetValue(null).ToString());
}
}
catch
{ }
}
}
}
static SortedSet<AcadProductKey> _apk = new();
/// <summary>
/// 获取注册表版本信息和安装路径
/// </summary>
/// <returns></returns>
static void GetAcadProductKeys()
{
var adsk = Registry.CurrentUser.OpenSubKey(@"Software\Autodesk\AutoCAD");
if (adsk == null) return;
foreach (string ver in adsk.GetSubKeyNames()) {
try
{
var emnuAcad = adsk.OpenSubKey(ver);
var curver = emnuAcad.GetValue("CurVer");
if (curver == null)
continue;
string app = curver.ToString();
string fmt = @"Software\Autodesk\AutoCAD\{0}\{1}";
emnuAcad = Registry.LocalMachine.OpenSubKey(string.Format(fmt, ver, app));
if (emnuAcad == null)
{
fmt = @"Software\Wow6432Node\Autodesk\AutoCAD\{0}\{1}";
emnuAcad = Registry.LocalMachine.OpenSubKey(string.Format(fmt, ver, app));
}
var acadLocation = emnuAcad.GetValue("AcadLocation");
if (acadLocation == null)
continue;
string location = acadLocation.ToString();
if (!File.Exists(location + "\\acad.exe"))
continue;
var produ = emnuAcad.GetValue("ProductName");
var release = emnuAcad.GetValue("Release");
if (release == null)
continue;
var pro = new AcadProductKey()
{
ProductKey = emnuAcad,
Location = location,
ProductName = produ?.ToString(),
};
pro.SetVersion(release)
apk.Add(pro);
}
catch { }
}
}
}
}
AcadClsIds.cs 收集Acad的Com接口
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace JoinBox.BasalRegistry {
public class AcadClsIdInfo
{
AcadProductKey _AcadProduct;
public AcadProductKey AcadProduct
{
get
{
if (_AcadProduct == null)
_AcadProduct = AcadRegedit.AcadProductKeys.First(a => a.Version == Version);
return _AcadProduct;
}
}
public double VerNumber
{
get
{
double.TryParse(Version, out double verdouble);
return verdouble;
}
}
string _Version;
public string Version
{
get
{
if (_Version == null)
{
foreach (var item in AcadRegedit.AcadProductKeys)
{
if (item.Guid == Guid)
{
_Version = item.Version;
break;
}
}
}
return _Version;
// return AcadClsId.GetAcadVer(Guid);
}
}
public Guid Guid { get; }
public object Com { get; }
public AcadClsIdInfo(Guid acadClsId, object acadCom, AcadProductKey acadProduct)
{
Guid = acadClsId;
Com = acadCom;
_AcadProduct = acadProduct;
}
public bool Equals(AcadClsIdInfo a, AcadClsIdInfo b)
{
return a.Version == b.Version ||
a.Version == b.AcadProduct.Version;
}
public int GetHashCode(AcadClsIdInfo obj)
{
return base.GetHashCode();
}
}
public class AcadClsIdHelper
{
//参考自 http://www.cadgj.com/?p=297
[DllImport("ole32.dll", EntryPoint = "CreateBindCtx")]
static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);
[DllImport("ole32.dll", EntryPoint = "GetRunningObjectTable")]
static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
/// <summary>
/// 遍历进程
/// </summary>
/// <param name="action">程序名,com接口</param>
public static void GetAllInstances(Action<string, object> action)
{
int retVal = GetRunningObjectTable(0, out IRunningObjectTable rot);
if (retVal != 0) return;
rot.EnumRunning(out IEnumMoniker enumMoniker);
var moniker = new IMoniker[1];
while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
{
CreateBindCtx(0, out IBindCtx bindCtx);
moniker[0].GetDisplayName(bindCtx, null, out string displayName);
rot.GetObject(moniker[0], out object comObj);
if (comObj is null)
continue;
action.Invoke(displayName, comObj);
}
}
/// <summary>
/// 获取已经启动的CAD进程的com
/// </summary>
/// <param name="apps">返回已经存在的参数</param>
/// <param name="acadClsId">指定版本</param>
public static void GetActiveAcadCom(List<AcadClsIdInfo> apps, Guid? acadClsId = null)
{
HashSet<Guid> guids = new();
if (acadClsId is null) {
var gs = AcadRegedit.AcadProductKeys.Select(a => a.Guid);
guids.AddRange(gs.ToArray());
}
else {
guids.Add(acadClsId.Value);
}
GetAllInstances((displayName, comObj) => {
#if DEBUG
// 不启动cad的时候运行一次,看看有什么GUID,然后再启动一个cad对比就知道了
Debug.WriteLine("****这个进程 " + displayName);
#endif
if (string.IsNullOrEmpty(displayName))
return null;
var guid = CreateGuid(displayName);
if (guids.Contains(guid.Value))
{
// 17.1存在的时候17也会存在,所以会有两个obj被保存
apps.Add(new AcadClsIdInfo(guid.Value, comObj, null));
}
});
}
/// <summary>
/// 显示名称转为Guid
/// </summary>
/// <param name="displayName">显示名称 "!{6AB55F46-2523-4701-2222-B226F46252BA}"</param>
/// <returns></returns>
static Guid CreateGuid(string displayName)
{
var displayName = displayName.Substring(2, displayName.Length - 3);
return guid = new Guid(displayName);
}
}
}
AcadStart.cs 启动cad
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
#if NET50
using Microsoft.VisualBasic;
#endif
namespace JoinBox.BasalRegistry
{
public static class StartApplicat
{
public static object Run(string progID, string exePath)
{
//处理 GetActiveObject 在电脑睡眠之后获取就会失败.所以要 ProcessStartInfo
//https://blog.csdn.net/yuandingmao/article/details/5558763?_t_t_t=0.8027849649079144
//string progID = "AutoCAD.Application.17.1";
//string exePath = @"C:\Program Files (x86)\AutoCAD 2008\acad.exe";
object acadApp = null;
acadApp = AcadBlockingStart(progID);
if (acadApp != null)
return acadApp;
// var psi = new ProcessStartInfo(exePath, "/p myprofile");//使用cad配置,myprofile是配置名称,默认就不写
var psi = new ProcessStartInfo(exePath, "/nologo")
{
WorkingDirectory = Environment.GetEnvironmentVariable("TEMP")//这里什么路径都可以的
};
var pr = Process.Start(psi);
pr.WaitForInputIdle();//这里并不会阻塞
int qidong = 0;
while (acadApp == null)
{
try
{
acadApp = AcadBlockingStart(progID);
}
catch
{
Application.DoEvents();
}
if (qidong == 20)
throw new ArgumentNullException("启动失败,错误计数已达最高");
++qidong;
Thread.Sleep(500);
}
return acadApp;
}
/// <summary>
/// 启动cad(阻塞)
/// </summary>
/// <returns></returns>
public static object AcadBlockingStart(string ProgID, string path = null)
{
object acadApp = null;
#if NET50
//两种方法,都会启动一个新的Acad.
int startWay = 1;//启动方式
if (startWay == 1)
{
//阻塞启动
acadApp = Interaction.CreateObject(ProgID);
}
else if (startWay == 2)
{
//阻塞启动
var comObjectName = Type.GetTypeFromProgID(ProgID);
if (comObjectName == null)
throw new ArgumentNullException($"本机不存在:{ProgID}");
acadApp = Activator.CreateInstance(comObjectName);
}
#else
//阻塞启动
acadApp = Marshal.GetActiveObject(ProgID);
#endif
return acadApp;
}
/// <summary>
/// 启动cad(非阻塞)
/// </summary>
/// <returns>进程id</returns>
public static int AcadAntiBlockingStart(string exePath)
{
#if NET50
//不阻塞启动
//var exePath = @"C:\Program Files\Autodesk\AutoCAD 2021\acad.exe";
//exePath = @"C:\Program Files (x86)\AutoCAD 2008\acad.exe";
int acadId = Interaction.Shell(exePath, AppWinStyle.NormalFocus); //正常大小启动,返回进程id
return acadId;
#else
// var psi = new ProcessStartInfo(exePath, "/p myprofile");//使用cad配置,myprofile是配置名称,默认就不写
var psi = new ProcessStartInfo(exePath, "/nologo")
{
WorkingDirectory = Environment.GetEnvironmentVariable("TEMP")//这里什么路径都可以的
};
var pr = Process.Start(psi);
// pr.WaitForInputIdle();//这里并不会阻塞
return pr.Id;
#endif
}
}
}
ProcessHelper.cs 进程工具类
using System.Security.Principal;
namespace JoinBox.BasalRegistry {
public static class ProcessHelper {
/// <summary>
/// 判断当前程序是否通过管理员运行
/// </summary>
/// <returns></returns>
public static bool IsAdministrator() {
var current = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(current);
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
}
}
}
Acad所有版本存放在这个注册表位置
计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\Hardcopy
var ackey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Autodesk\Hardcopy", false);
var values = ackey.GetValueNames();
测试VB通过文档名获取Com实例
cad内部调用com接口是可以区分同cad版本
的,但是外部调用要经过系统的com服务.
南胜告诉我们,
在VB里面调用Microsoft.VisualBasic.Interaction.GetObject(文档路径)
传入一个已经被程序打开的文档路径,是可以获取同版本cad的com
的.
而没有保存文档怎么办?
答案是我不知道,但是我也大胆的猜测一下,cad打开dwt模板的时候会在系统临时文件夹创建一个缓存文件,我们找到这个缓存文件然后跟开图一样Interaction.GetObject问询它占用的程序com?
有个问题
测试时候发现断点会出现一个"把其他cad的文档抓过来第一个开的cad"上面,
然后呈现只读文档,而直接运行就不会.
我大胆的假设一下:
com需要连续的报文,断点调试等于服务器收不到信号,
程序认为没打开这个文档,帮你开一个,
然后触发了dwg打开的流程=>
先再注册表找.dwg,再经过<桌子的dwg启动服务>,
然后让已经启动的<第一个开的cad>承接打开这个dwg.
测试代码
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
#if NET50
using Microsoft.VisualBasic;
#endif
namespace 测试_exe发送cad {
static class Program {
[STAThread]
static void Main() {
// 开启两个 已经保存文档 的同版本cad,
// 然后启动这个测试代码,
// 第一个启动的cad上面有 "第二个cad的文档名称-只读" (断点触发,并且切换一下文档才会冒出[只读]的文字)
//获取所有acad的进程句柄
var acadPros = Process.GetProcessesByName("acad");
foreach (var acadPro in acadPros)
{
//fn = "D:\\桌面\\asdasdasd.dwg"
var fn = acadPro.MainWindowTitle
.GetSpliterRightText("[").Replace("]", "").Trim();
if (!File.Exists(fn)) //跳过 drawing* 2008测试不需要,高版本需要
return;
dynamic acadCom = Interaction.GetObject(fn);
dynamic acadApp = acadCom.Application;
foreach (var doc in acadApp.Documents)
{
string fullName = doc.FullName;
string name = doc.Name;
var str = $"****进程{acadPro.Id}::FullName:{fullName}, Name:{name}";
if (doc.ReadOnly)
Debug.WriteLine(str + "-------只读文档!");
else
Debug.WriteLine(str + "不是只读文档");
}
}
}
}
public static class StrSpliterHelper
{
/// <summary>
/// 字符串有分隔符
/// </summary>
/// <param name="str"></param>
/// <param name="spliter"></param>
/// <returns></returns>
public static bool HasSpliter(this string str, string spliter)
=> str.Contains(spliter);
/// <summary>
/// 获取分割符左侧的部分
/// </summary>
/// <param name="str"></param>
/// <param name="spliter"></param>
/// <returns></returns>
public static string GetSpliterLeftText(this string str, string spliter)
=> str.HasSpliter(spliter) ?
str.Substring(0, str.IndexOf(spliter, StringComparison.Ordinal)) : str;
/// <summary>
/// 获取分隔符右侧的字符串
/// </summary>
/// <param name="str"></param>
/// <param name="spliter"></param>
/// <returns></returns>
public static string GetSpliterRightText(this string str, string spliter)
=> str.HasSpliter(spliter) ?
str.Substring(str.IndexOf(spliter, StringComparison.Ordinal) + 1,
str.Length - str.IndexOf(spliter, StringComparison.Ordinal) - 1)
: str;
}
}
(完)