From 665a722f685fc8b5a2678b75d9884d5dc84fbb99 Mon Sep 17 00:00:00 2001 From: fengjiayi <12821976+ning_xi@user.noreply.gitee.com> Date: Thu, 2 Jan 2025 13:57:14 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Serein.Workbench.Avalonia.Android/Icon.png | Bin 0 -> 14349 bytes .../MainActivity.cs | 22 +++++++ .../Properties/AndroidManifest.xml | 5 ++ .../Resources/drawable/splash_screen.xml | 13 ++++ .../Resources/values-night/colors.xml | 4 ++ .../Resources/values/colors.xml | 4 ++ .../Resources/values/styles.xml | 12 ++++ .../Serein.Workbench.Avalonia.Android.csproj | 28 +++++++++ Serein.Workbench.Avalonia.Browser/Program.cs | 17 +++++ .../Properties/AssemblyInfo.cs | 1 + .../Properties/launchSettings.json | 13 ++++ .../Serein.Workbench.Avalonia.Browser.csproj | 15 +++++ .../runtimeconfig.template.json | 10 +++ .../wwwroot/app.css | 58 ++++++++++++++++++ .../wwwroot/favicon.ico | Bin 0 -> 176111 bytes .../wwwroot/index.html | 36 +++++++++++ .../wwwroot/main.js | 13 ++++ 17 files changed, 251 insertions(+) create mode 100644 Serein.Workbench.Avalonia.Android/Icon.png create mode 100644 Serein.Workbench.Avalonia.Android/MainActivity.cs create mode 100644 Serein.Workbench.Avalonia.Android/Properties/AndroidManifest.xml create mode 100644 Serein.Workbench.Avalonia.Android/Resources/drawable/splash_screen.xml create mode 100644 Serein.Workbench.Avalonia.Android/Resources/values-night/colors.xml create mode 100644 Serein.Workbench.Avalonia.Android/Resources/values/colors.xml create mode 100644 Serein.Workbench.Avalonia.Android/Resources/values/styles.xml create mode 100644 Serein.Workbench.Avalonia.Android/Serein.Workbench.Avalonia.Android.csproj create mode 100644 Serein.Workbench.Avalonia.Browser/Program.cs create mode 100644 Serein.Workbench.Avalonia.Browser/Properties/AssemblyInfo.cs create mode 100644 Serein.Workbench.Avalonia.Browser/Properties/launchSettings.json create mode 100644 Serein.Workbench.Avalonia.Browser/Serein.Workbench.Avalonia.Browser.csproj create mode 100644 Serein.Workbench.Avalonia.Browser/runtimeconfig.template.json create mode 100644 Serein.Workbench.Avalonia.Browser/wwwroot/app.css create mode 100644 Serein.Workbench.Avalonia.Browser/wwwroot/favicon.ico create mode 100644 Serein.Workbench.Avalonia.Browser/wwwroot/index.html create mode 100644 Serein.Workbench.Avalonia.Browser/wwwroot/main.js diff --git a/Serein.Workbench.Avalonia.Android/Icon.png b/Serein.Workbench.Avalonia.Android/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41a2a618fb02e4cb7f6a15caf572b693bfe1ebb1 GIT binary patch literal 14349 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 +{ + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } +} diff --git a/Serein.Workbench.Avalonia.Android/Properties/AndroidManifest.xml b/Serein.Workbench.Avalonia.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000..a561c56 --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Serein.Workbench.Avalonia.Android/Resources/drawable/splash_screen.xml b/Serein.Workbench.Avalonia.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000..2e920b4 --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/Serein.Workbench.Avalonia.Android/Resources/values-night/colors.xml b/Serein.Workbench.Avalonia.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000..3d47b6f --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/Serein.Workbench.Avalonia.Android/Resources/values/colors.xml b/Serein.Workbench.Avalonia.Android/Resources/values/colors.xml new file mode 100644 index 0000000..59279d5 --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/Serein.Workbench.Avalonia.Android/Resources/values/styles.xml b/Serein.Workbench.Avalonia.Android/Resources/values/styles.xml new file mode 100644 index 0000000..6e534de --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Resources/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/Serein.Workbench.Avalonia.Android/Serein.Workbench.Avalonia.Android.csproj b/Serein.Workbench.Avalonia.Android/Serein.Workbench.Avalonia.Android.csproj new file mode 100644 index 0000000..94feb2b --- /dev/null +++ b/Serein.Workbench.Avalonia.Android/Serein.Workbench.Avalonia.Android.csproj @@ -0,0 +1,28 @@ + + + Exe + net8.0-android + 21 + enable + com.CompanyName.AvaloniaTest + 1 + 1.0 + apk + False + + + + + Resources\drawable\Icon.png + + + + + + + + + + + + diff --git a/Serein.Workbench.Avalonia.Browser/Program.cs b/Serein.Workbench.Avalonia.Browser/Program.cs new file mode 100644 index 0000000..3fd277e --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/Program.cs @@ -0,0 +1,17 @@ +using System.Runtime.Versioning; +using System.Threading.Tasks; + +using Avalonia; +using Avalonia.Browser; + +using Serein.Workbench.Avalonia; + +internal sealed partial class Program +{ + private static Task Main(string[] args) => BuildAvaloniaApp() + .WithInterFont() + .StartBrowserAppAsync("out"); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(); +} diff --git a/Serein.Workbench.Avalonia.Browser/Properties/AssemblyInfo.cs b/Serein.Workbench.Avalonia.Browser/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..fb78795 --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly:System.Runtime.Versioning.SupportedOSPlatform("browser")] diff --git a/Serein.Workbench.Avalonia.Browser/Properties/launchSettings.json b/Serein.Workbench.Avalonia.Browser/Properties/launchSettings.json new file mode 100644 index 0000000..33ed958 --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Serein.Workbench.Avalonia.Browser": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:7169;http://localhost:5235", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + } + } +} diff --git a/Serein.Workbench.Avalonia.Browser/Serein.Workbench.Avalonia.Browser.csproj b/Serein.Workbench.Avalonia.Browser/Serein.Workbench.Avalonia.Browser.csproj new file mode 100644 index 0000000..a8f3adc --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/Serein.Workbench.Avalonia.Browser.csproj @@ -0,0 +1,15 @@ + + + net8.0-browser + Exe + true + + + + + + + + + + diff --git a/Serein.Workbench.Avalonia.Browser/runtimeconfig.template.json b/Serein.Workbench.Avalonia.Browser/runtimeconfig.template.json new file mode 100644 index 0000000..b96a943 --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/runtimeconfig.template.json @@ -0,0 +1,10 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "browser", + "host": "browser" + } + ] + } +} \ No newline at end of file diff --git a/Serein.Workbench.Avalonia.Browser/wwwroot/app.css b/Serein.Workbench.Avalonia.Browser/wwwroot/app.css new file mode 100644 index 0000000..1d6f754 --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/wwwroot/app.css @@ -0,0 +1,58 @@ +/* HTML styles for the splash screen */ +.avalonia-splash { + position: absolute; + height: 100%; + width: 100%; + background: white; + font-family: 'Outfit', sans-serif; + justify-content: center; + align-items: center; + display: flex; + pointer-events: none; +} + +/* Light theme styles */ +@media (prefers-color-scheme: light) { + .avalonia-splash { + background: white; + } + + .avalonia-splash h2 { + color: #1b2a4e; + } + + .avalonia-splash a { + color: #0D6EFD; + } +} + +@media (prefers-color-scheme: dark) { + .avalonia-splash { + background: #1b2a4e; + } + + .avalonia-splash h2 { + color: white; + } + + .avalonia-splash a { + color: white; + } +} + +.avalonia-splash h2 { + font-weight: 400; + font-size: 1.5rem; +} + +.avalonia-splash a { + text-decoration: none; + font-size: 2.5rem; + display: block; +} + +.avalonia-splash.splash-close { + transition: opacity 200ms, display 200ms; + display: none; + opacity: 0; +} diff --git a/Serein.Workbench.Avalonia.Browser/wwwroot/favicon.ico b/Serein.Workbench.Avalonia.Browser/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 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 zds⪻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

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF zk`(0Tj<ZW5E8&DpXim*G3_g%p~m+WPP3v>mv`TF{tA#CZlAV1iI z$sbTihG%;$dY@ID{z$-{<*^l96ez;5-yPlm9<#0Vp(DmRMjt5}mO&)*|Ao}9w1NT!U!3v{BR>pG3m4dvAv-jR< zR##iwb^`%3Z8MMRLF*iNu zmF$BJptdopP98FT%?meHALC3)l*?Eak7wgYuQrXJ_Eu;ZwiG;`^w{1|Op42fT9g|_ zsshz40(mPpNJVTIg;l?C(@J!tO!HQn8OY;fg^<T3@U|Sc83Nq zOA#FoV9fdJ-PZT9?Dwwq6Kp+yW$6+j$E+2gEKV)%rCetTnm49eyPU7DZk#q%+pW~y z|B0KsLwG6D4WtEQsJaB!E_;~4mYqb}Yk`PDRW*g$2-eD;kqA^yXCsWBh2TB7O9xEH zMx{eH8;>r~9Zc${$noP^Mu!$ThYQfe*2=(d(b)gw;QQqxDX3?It`b{AekTLUF7Gb?yx`*nAz;?yh5 z7*w$JXKH%7VCZ3i~H`= zlE&Bef$)~x%WH6)Z!*-LrE7XzE52Q^$yk%qEh{E$wz}DjokiB|S1m0hvNysa2*7yx zRs-~^|HTL(s$@{Nh>l8Kiq&75AW8<|9L$KoUWACkuW*yoq&mH?Kuqg+SJ6RO&dhv<{j z_$#!^w$(5>*r|^qC%a+CrN&>={f#9E(8 zJq;|$^KYMHGhb#$GV`L9DA&Tu9WVS=`^R-~QI^QPnguaC>2Sx$j= z-DutV&bp-LooA|saXYm0#D?5tVq=*c1p_JM)&sN!Dh&h8$0s8+pI&UM5-VKtLR^|f zR6s3K=#)Hww>x}V{+W1FY5O9Ni&J-Te0)w}mk>YIY2Ed=li}W)<=Itt!TYdl7=Sh z;#qSU=Y^){>z%OJwY9FtNm9bWx!lVEY)KUXd6US~c+`>?LTC%RKVj5XescPDCNiH? zhV{psT50&XMGX!8$9>GU06DboPbf-;-0P_;zwX6JY=spxmQ`z`k{a_CoKYvuhcgq% zgGHcIBXvS5q}ECb@4U`Uv3cE^d^1{?t!u$Wa;s?YK3?q!GiIHHU~SSePBrQbcrT5l z8M7Uyd3zr$y^wcP^4z>MVgQId{@6WNWId7%zhfB)DWt2*V6E}%sYf?CA#7BqKby!E zU`eWB^{=NNul)2nr$iTYbH8GLlSZZt!UO%d;Gt)uXYZ?PO5s?9?A(C0%~LOn+LFYj zHJ8#<=lC|m+5=%H0=Ljrao{)uL_UwRPr~}iSWP>_`@!$uN^%{oWBg@5H}L$#;Jgdf z!IXj`;4o57A;S-*j_r!|EKOdBhcG&#e_(%em3P_vDnCUDx8|3VoIqSZfy-a4*ZSFi z#>Vwd(~o)X^;%H3HdsLQ)%uy?KJ$Ks4%*DVf(g|Of%ZgWvHhXL5>dX|b=*{QpTuS< zY+o-T7n6<7X)^uUH98tYoijiGlpxU8=iLh%+h`TXTx8j@M=NbKq{zu}{EMHE&bJ&f zAvqz1V6+C09m3dYl2P1^)rViuhQ9oUch_HpDv(~XwWJ(y(Lw-#dmXkHB}mJ|u;={x zv)>iJy8ehjP`Ptfx=}ZLJrfVz4?8rtG}uDPDR%?+|^7l}C~k1Va1A4ABL*j4-5YPgh@0zLBW4rw7M~5mML5 zU!bcsd?0e)a*n=uTDm(cjd0oa)goDH2@JvjqT{!l9z5|MwH$j-HfjtOJPEe6xw&HA zDzIn5mD83mi{T8({+5!(lVr1+%ufiGfG;>$l57u3ymgDvWU zPfwnwk6F#JaGP5ya|5z-+dBNVSr)5JzkXO7?QI@E&vE$e3${65=*1sZ1fr=ziku@< zjp59%EX{DohXgIT^5uS*`h-g$`#Ch7w?%%4F6?c$ta`y)eHe%+QEVtHdycoF>h z>`C=EU!DYpHK9;x@0ss#bP)NseFhK8=&e;J!@F*eTlpZWJDr&>T+)`zhv~xpkd@?P zQ5EB<#nIV{q$a?8i@cpKmgTSwTpjDk!xJBx{Xo}3#vHxLOR>eD{GF$r|E^dK2=taUu+Z1BeUQ&{hSd$r1wazZe!Q@+T17Tv1}>tf2fd$2Nyn?U$JsM zyB!4eW0HQ#l)ln>Wt^!eESx#I=t1!u;(-OR zDi4rxJ1lGCK7(2>UCrQt9fSKQKgXhyV{00YtH5c*kiA?>!o#Y21M6-c>1(GS&vb?F z9yE4|k_CZ*+RTjnz24dQR$62HNpv3@#JNtx=XZhbXU{9Al6fbRd8hr&ge*AL^!A^9 z^U@3Zd)NFe-@uL9Ourm{4U}GIi@g{AN(4zuk}-o1#ck|)%75<}_}9?Q^QNBbQCu&D~>g|fXS&?W#NlX+5tOBn*Rw|xJT!O2)0z3GsE5^92gs8eQHcPOZ6*%RHF^Y6E_UexI^;&$1e65lU8 zzURAt@cfJ0&H~>Rci8bHc)f|H>;S7Splj*yt|YG z`VqJOyMSMgSBmLAC8MjFaf*umWyrGe)rW~+atV$GQ%a2SPii-~BUrA!*VhR?7FL`F zkiI!iUflzM>FUO%`#ASQPuj8XbDU}d<=UNWq%{tXb7F8~VgGVO4K5mhK}0gNTIuVv zbu}2c{L<(GpYN3|^iIlX*9mQd9UZw#X0FREN$XWSt}9&F9ur`kTp}?G0|&AFIObV;}q{3H7ifApmbTo~QAxrwPNsggCKC z%C*}lG-p1?$%34*^c7n7g_zzyPqGA^oWo}amf!`cU~q{Z?|mWzKzMR{I`2Bcef2*k zxF4R>En2%3i^?wLg2~Z{PVHi@}_fmO8#q58n^EZvC zaZGjXuko-eXOFYtQr9!JH92}?8jo^=?2Lwdc^`6cuoE^aT;)rJ3`V|%A-c8Bhn~(C zo-D>^p@?^gq64cD*4jHdGQavS4W?FEo_S-Lui3k^l&Wr_QUGNEe9PeO`;m`1If9`p zR~sKiEWA_qGtY{<*dVIh1)KfnS_Tq1u(iy}Dq<>!jAFkbLdhr-atXfsP5Eclsj!Rh z8|yNiG00v9i|rp@Ixc#0UZ%M=43Mp}#k1XnluVb#$j?kB7?)(1Zr#scqFzJ>;_vug zNTm^g{^aY#2)Lv+x%3)OiJ1ihh^?V(`|)~88X0WDitL_fdOaO3PK?<>K`2g)OXRF} z?yB##-p;0_70X&NTb0T+e4sUy=2sZ|@I0(e9RGZY5mGF;NzOK(^v-Cyxa%F950!K< zXS=z07oU8?QasUbu)6ZpEVK_=11R%L9!%(2PclE#2b6Z9uDD%ams-~BqD5>D6~fGH#FaxfGEwGY)El-(V?X`)dIfhir*9wFvi!M>Hd=|o zZR41~CGOd!&1y}aT=DJx+s2iu3-);X78#0PcPdkg9pokYOz{Kq*UV_+#8yYrmsTI zh{m(w`ao`s0^ek=1|crEcTPYn7kahJ4(OQ-v%^NmMG)0iou zi+A?|zsN`Y;6F{vO}W{3;57*Ww-%S8i{ZzOaK4a$lHxfWEkwRoyMLM1`ek`2ZjC{A zvN-f8)c`{;SK>zW$nR@pEwgz`s3OSY)9bdpr&<4tqZDbnA-6aL^sEtQplCDIk3;0s=4C zSZ!%Be>8dw->q-2iUW0s=WSIC)nGV-NZIE$iXud120g7GJyK8n?&JPHXRbY~8s-vz z3+}%p6wnG`w%I?P>=`{uExEOr$XyU!Fy0Hq(=quGX3a*J|FzFbTVy(MH{MLILsLY2 zAa{&!F?Y07%Iouf{w@n5wqfT89~yAFck4ae3Q;o#ypX~}wL-adz+({02ZUyt`{vt6~~LQ6sfJb|+Ap4Jx3b)0?@``W9(A4h%AknA+m4TvJ@d6JMrj=!H{KyB4lYol(CCs$yl-*OAoS>wMcd; z@=ovjFTB@vu5*4p=f3aj+@H@m_vfz9Yc|Gbo9tYDNzd*ERxWShzDYjGrzN&;m^1Ip z9@P)QcQbdi@!d)O^3P_B1?VT6ZmMSh)E~#OL$8^xduQYc?~OIQ`Fkb(%p< z)FXMnc=dddZD%ix=jBXF=iVB+fH4}ZoHm@VZLm=)URkHeRazxApF&k@=Yp&2`capl zJq*!Gu@@NB#zQjhxBm>JWl`$t5TUp}w8#)N#?qq0`K0g*Z?V>Xq9PQJdUthN}xe#%^jYy6N5K@^*a zf13LBH#zJb-5IqubNR9#`^CG$d`WKh>>P8SZ+y zGF_OG;hkX@U-b=rFEiiC7-w@mj`{msUwTWL%xUEG_nn9U?jJ|>*8!=#Yiw{ZFh|U7 zZ(f{@^d@TK#&42?G>5tV8ZHUJ@P<+!m~YBHD_IQ{gcAO(@8tu#Az@J2>|TOZUpE)# z0)H)jOzM?G3`h~;k^ZkDFZk8i!Nv;N+NmnN*}3!`Ce{(XlJRLsLW%Xo9v>Y+7OAb^ zgxebXY!45DF8#7Kem4v$b90c;_cm^TE4-1ue#XJm#n>SO%RR|C`I^n>z+WwizJ&Q6 zYDVHxX8IZ%c7M_=rj{cd>Cw2mxp6CMwBrQFxEjXHWDhp1G_sc$D<$6NS7va(VZO;K zQ$AF+N>jb;dd(AP1$bRb`Jy`sx7w+(Njy50R9D%Q9ty@8o@$20;h9)irTN5_N&N|) znV%Yjd>bbn{H&V2;odb187WnRhG4M`(c2X{MK+wm&r9mmmi!I3>zrbpE_C+2RsUX0 z9$J>tGT5|ygY0qGo4hmN{b=%KO5>8c@Y7*tqsSVzMfJy58lI^5_!drmsml>qkI;R; z|KHSyRvVqYS`sop&k%_+o#tb&?QOngM5RvygtoH+UfvT{ZMv-BD20Q1a&`jiD>Kvf zjZ6|Bqawbe!xI=nAt)sZL{glgtBn<0XiyXl7C8@7l4k%-BrtbAV^y4%#A)horoIq5 zH>PI;Cg`A}0VFWAt%!jHq&Gr-Y&kMPEUg>{ON)^VNN42=07izvU~wQydQh!mcz&)h z`m0iPi>wgVBs|A9K=Fti!fhgSL*rz)V?TKm@=<0(5ycKd10JxuQKWy4;f!WT(~1)9 znu`{+Gc12E;hLsYW@a{a4Keu=Ng$4ZrvOY#l119B;&BrY{JG%JOoMUTdky=S1(YM& z*PJw<8hpv{b#v}X>R@_>al@|#CTh)2*?*A=_b9j>puWS%x5SROG{GPRM@)`^kzr2X zZlztAG&S+BF{!sKXMR*NnVM1=7z-f^Ub(?uN2?@MR>dMSC**5jH6l0MB*Z1SZWN7P z5)nO5L7x*zl;L;Lv&uxUr0QE)tK;!lCI}v@&4DyWa+vkn+SHiSvc%84mU)4emO*gH z$RH4Oc#0^M7+e8iEdd6?sEOFfXeoe_27pwV?(IcA7z)$1UX~YlstnaEq3jBRMvfQrDbyL{vA;O|7sRvqy@88NY)8)r+6HVnUi*oRBMU6cBdIv*6h! zS?zcgJW?`L;Rxp3d5tzM#y(Y6w1>TZ32o@BwN#Mc?YPvrEYdhP=ErB`^^; z&Vi^jgnWF5EXz5pF`37rR~pvAc_R|@B)N@ms>pzgqMDcpd7^XHEwf{;@9^MzyuPm1 zank9;g=4Qj_KQA=#Q=Vx_0bg|YD31np_B*Hr>Tuwg?d(}e4Q6ez)7ytjD{SqzN%e+ z+&%ggAfLjj*V2{IF zf0V&0i0NLcpS)HMpcO0Lk``KbvZ6}y4U-fwd)3PR6+;*cAyXM=huqyi|8ZN+p{~8wLc>HfH|>i9p~F!B3>AL-QaHRQt0d|VSP2u6|W6p>ITK3rQ)EE zhvjj)cus9Mrc@Gjr7-xbfA<^L7X`_=?ItsS=skkk&iloos&DSaQG&DLT zOI{z1Zj+@%pWx#tM|eGAJiCI3+gEUhQeL)X?7UcU8@L z+#q=60Mf6 zT1pdtf`IhqRHv9bcEk|OC0=YaqEfO+h(IN8SC}!Jv^?)1IRtl&xL-OR;ZJVN2nlv~ zzh;s<$hIZG`MZ`*$v(dgDarre%yM@`=x__0TA&BK^hlV5>WJmQ(>DCAK)jz{^hRNHj_HWX4zN^0(Ci^qt_IOHLwE!ZgPy?GEn{)>N(5xCD0KrXJxI}6F zM)}xiL88)b@o0I7=Y+uR2R^wICq0fc5u5kk%RQ^J&%5j8ux7l{{M`=4droTKmPtaPfGI|6j`F z6T!vd*UKki^>*Q*5fkKDQ-Zp1Skl03k&wpH{$=K=!Tm>vANw5{dE zFGXJL^NRGI5L4#?s*dXD*e1gs7*ea5LTPED*hbKWDKVj&Bg}xlGJ9Hx3r-bL~Gy) z)(cjuf?Po7%>I)vF<0B>e(^+1PqEpklwSNDo4a_CF18wPc^Bt2S07$Y8%W+h + + + + Serein.Workbench.Avalonia + + + + + + +
+
+

+ Powered by + + + + + + + + + + + + + + +

+
+
+ + + + diff --git a/Serein.Workbench.Avalonia.Browser/wwwroot/main.js b/Serein.Workbench.Avalonia.Browser/wwwroot/main.js new file mode 100644 index 0000000..bf1555e --- /dev/null +++ b/Serein.Workbench.Avalonia.Browser/wwwroot/main.js @@ -0,0 +1,13 @@ +import { dotnet } from './_framework/dotnet.js' + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const dotnetRuntime = await dotnet + .withDiagnosticTracing(false) + .withApplicationArgumentsFromQuery() + .create(); + +const config = dotnetRuntime.getConfig(); + +await dotnetRuntime.runMain(config.mainAssemblyName, [globalThis.location.href]); From c47320d4de3dc583dad595797e4e09a8ff2581fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E6=B3=93=E7=A7=8B=E6=B0=B4?= <1090698674@qq.com> Date: Thu, 2 Jan 2025 13:58:40 +0800 Subject: [PATCH 2/4] Delete WorkBench directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除不要的项目文件夹 --- WorkBench/App.xaml | 22 - WorkBench/App.xaml.cs | 75 - WorkBench/AssemblyInfo.cs | 10 - WorkBench/LogWindow.xaml | 24 - WorkBench/LogWindow.xaml.cs | 162 - WorkBench/MainWindow.xaml | 259 -- WorkBench/MainWindow.xaml.cs | 3027 ----------------- WorkBench/MainWindowViewModel.cs | 101 - WorkBench/Node/NodeControlViewModelBase.cs | 48 - WorkBench/Node/View/ActionNodeControl.xaml | 118 - WorkBench/Node/View/ActionNodeControl.xaml.cs | 83 - WorkBench/Node/View/ConditionNodeControl.xaml | 110 - .../Node/View/ConditionNodeControl.xaml.cs | 58 - .../Node/View/ConditionRegionControl.xaml | 22 - .../Node/View/ConditionRegionControl.xaml.cs | 94 - WorkBench/Node/View/DllControlControl.xaml | 34 - WorkBench/Node/View/DllControlControl.xaml.cs | 164 - WorkBench/Node/View/ExpOpNodeControl.xaml | 47 - WorkBench/Node/View/ExpOpNodeControl.xaml.cs | 56 - WorkBench/Node/View/FlipflopNodeControl.xaml | 110 - .../Node/View/FlipflopNodeControl.xaml.cs | 72 - .../ViewModel/ActionNodeControlViewModel.cs | 13 - .../ConditionNodeControlViewModel.cs | 54 - .../ConditionRegionNodeControlViewModel.cs | 18 - .../ViewModel/FlipflopNodeControlViewModel.cs | 14 - .../Node/ViewModel/TypeToStringConverter.cs | 27 - WorkBench/Serein.WorkBench.csproj | 74 - .../Serein.WorkBench_d2hd4tgu_wpftmp.csproj | 288 -- WorkBench/Themes/IOCObjectViewControl.xaml | 28 - WorkBench/Themes/IOCObjectViewControl.xaml.cs | 128 - WorkBench/Themes/InputDialog.xaml | 16 - WorkBench/Themes/InputDialog.xaml.cs | 42 - WorkBench/Themes/MethodDetailsControl.xaml | 129 - WorkBench/Themes/MethodDetailsControl.xaml.cs | 90 - WorkBench/Themes/NodeTreeItemViewControl.xaml | 59 - .../Themes/NodeTreeItemViewControl.xaml.cs | 280 -- WorkBench/Themes/NodeTreeViewControl.xaml | 47 - WorkBench/Themes/NodeTreeViewControl.xaml.cs | 85 - WorkBench/Themes/ObjectViewerControl.xaml | 31 - WorkBench/Themes/ObjectViewerControl.xaml.cs | 670 ---- WorkBench/Themes/TypeViewerWindow.xaml | 16 - WorkBench/Themes/TypeViewerWindow.xaml.cs | 279 -- WorkBench/Themes/WindowDialogInput.xaml | 30 - WorkBench/Themes/WindowDialogInput.xaml.cs | 70 - .../InvertableBooleanToVisibilityConverter.cs | 41 - .../Tool/Converters/ThumbPositionConverter.cs | 79 - .../Tool/Converters/TypeToColorConverter.cs | 26 - 47 files changed, 7330 deletions(-) delete mode 100644 WorkBench/App.xaml delete mode 100644 WorkBench/App.xaml.cs delete mode 100644 WorkBench/AssemblyInfo.cs delete mode 100644 WorkBench/LogWindow.xaml delete mode 100644 WorkBench/LogWindow.xaml.cs delete mode 100644 WorkBench/MainWindow.xaml delete mode 100644 WorkBench/MainWindow.xaml.cs delete mode 100644 WorkBench/MainWindowViewModel.cs delete mode 100644 WorkBench/Node/NodeControlViewModelBase.cs delete mode 100644 WorkBench/Node/View/ActionNodeControl.xaml delete mode 100644 WorkBench/Node/View/ActionNodeControl.xaml.cs delete mode 100644 WorkBench/Node/View/ConditionNodeControl.xaml delete mode 100644 WorkBench/Node/View/ConditionNodeControl.xaml.cs delete mode 100644 WorkBench/Node/View/ConditionRegionControl.xaml delete mode 100644 WorkBench/Node/View/ConditionRegionControl.xaml.cs delete mode 100644 WorkBench/Node/View/DllControlControl.xaml delete mode 100644 WorkBench/Node/View/DllControlControl.xaml.cs delete mode 100644 WorkBench/Node/View/ExpOpNodeControl.xaml delete mode 100644 WorkBench/Node/View/ExpOpNodeControl.xaml.cs delete mode 100644 WorkBench/Node/View/FlipflopNodeControl.xaml delete mode 100644 WorkBench/Node/View/FlipflopNodeControl.xaml.cs delete mode 100644 WorkBench/Node/ViewModel/ActionNodeControlViewModel.cs delete mode 100644 WorkBench/Node/ViewModel/ConditionNodeControlViewModel.cs delete mode 100644 WorkBench/Node/ViewModel/ConditionRegionNodeControlViewModel.cs delete mode 100644 WorkBench/Node/ViewModel/FlipflopNodeControlViewModel.cs delete mode 100644 WorkBench/Node/ViewModel/TypeToStringConverter.cs delete mode 100644 WorkBench/Serein.WorkBench.csproj delete mode 100644 WorkBench/Serein.WorkBench_d2hd4tgu_wpftmp.csproj delete mode 100644 WorkBench/Themes/IOCObjectViewControl.xaml delete mode 100644 WorkBench/Themes/IOCObjectViewControl.xaml.cs delete mode 100644 WorkBench/Themes/InputDialog.xaml delete mode 100644 WorkBench/Themes/InputDialog.xaml.cs delete mode 100644 WorkBench/Themes/MethodDetailsControl.xaml delete mode 100644 WorkBench/Themes/MethodDetailsControl.xaml.cs delete mode 100644 WorkBench/Themes/NodeTreeItemViewControl.xaml delete mode 100644 WorkBench/Themes/NodeTreeItemViewControl.xaml.cs delete mode 100644 WorkBench/Themes/NodeTreeViewControl.xaml delete mode 100644 WorkBench/Themes/NodeTreeViewControl.xaml.cs delete mode 100644 WorkBench/Themes/ObjectViewerControl.xaml delete mode 100644 WorkBench/Themes/ObjectViewerControl.xaml.cs delete mode 100644 WorkBench/Themes/TypeViewerWindow.xaml delete mode 100644 WorkBench/Themes/TypeViewerWindow.xaml.cs delete mode 100644 WorkBench/Themes/WindowDialogInput.xaml delete mode 100644 WorkBench/Themes/WindowDialogInput.xaml.cs delete mode 100644 WorkBench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs delete mode 100644 WorkBench/Tool/Converters/ThumbPositionConverter.cs delete mode 100644 WorkBench/Tool/Converters/TypeToColorConverter.cs diff --git a/WorkBench/App.xaml b/WorkBench/App.xaml deleted file mode 100644 index baa41de..0000000 --- a/WorkBench/App.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - diff --git a/WorkBench/App.xaml.cs b/WorkBench/App.xaml.cs deleted file mode 100644 index a90c015..0000000 --- a/WorkBench/App.xaml.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Newtonsoft.Json; -using Serein.Library; -using Serein.Library.Utils; -using System.Diagnostics; -using System.IO; -using System.Windows; - -namespace Serein.Workbench -{ - - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - private async Task LoadLocalProjectAsync() - { - -#if DEBUG - if (1 == 1) - { - // 这里是测试代码,可以删除 - string filePath; - filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\net8.0\PLCproject.dnf"; - filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\banyunqi\project.dnf"; - filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\project.dnf"; - //filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\test.dnf"; - string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容 - App.FlowProjectData = JsonConvert.DeserializeObject(content); - App.FileDataPath = System.IO.Path.GetDirectoryName(filePath)!; // filePath;// - var dir = Path.GetDirectoryName(filePath); - } -#endif - } - - public static SereinProjectData? FlowProjectData { get; set; } - public static string FileDataPath { get; set; } = ""; - - private async void Application_Startup(object sender, StartupEventArgs e) - { - // 检查是否传入了参数 - if (e.Args.Length == 1) - { - // 获取文件路径 - string filePath = e.Args[0]; - // 检查文件是否存在 - if (!System.IO.File.Exists(filePath)) - { - MessageBox.Show($"文件未找到:{filePath}"); - Shutdown(); // 关闭应用程序 - return; - } - - try - { - // 读取文件内容 - string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容 - FlowProjectData = JsonConvert.DeserializeObject(content); - FileDataPath = System.IO.Path.GetDirectoryName(filePath) ?? ""; - } - catch (Exception ex) - { - MessageBox.Show($"读取文件时发生错误:{ex.Message}"); - Shutdown(); // 关闭应用程序 - } - - } - await this.LoadLocalProjectAsync(); - - - } - } - -} - diff --git a/WorkBench/AssemblyInfo.cs b/WorkBench/AssemblyInfo.cs deleted file mode 100644 index b0ec827..0000000 --- a/WorkBench/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/WorkBench/LogWindow.xaml b/WorkBench/LogWindow.xaml deleted file mode 100644 index 29bc07e..0000000 --- a/WorkBench/LogWindow.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/WorkBench/LogWindow.xaml.cs b/WorkBench/LogWindow.xaml.cs deleted file mode 100644 index 495a979..0000000 --- a/WorkBench/LogWindow.xaml.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System.Windows; - -namespace Serein.Workbench -{ - /// - /// DebugWindow.xaml 的交互逻辑 - /// - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using System.Timers; - using System.Windows; - - /// - /// LogWindow.xaml 的交互逻辑 - /// - public partial class LogWindow : Window - { - private StringBuilder logBuffer = new StringBuilder(); - private int logUpdateInterval = 200; // 批量更新的时间间隔(毫秒) - private Timer logUpdateTimer; - private const int MaxLines = 1000; // 最大显示的行数 - private bool autoScroll = true; // 自动滚动标识 - private int flushThreshold = 5; // 设置日志刷新阈值 - private const int maxFlushSize = 1000; // 每次最大刷新字符数 - - public LogWindow() - { - InitializeComponent(); - - // 初始化定时器,用于批量更新日志 - logUpdateTimer = new Timer(logUpdateInterval); - logUpdateTimer.Elapsed += (s, e) => FlushLog(); // 定时刷新日志 - logUpdateTimer.Start(); - - // 添加滚动事件处理,判断用户是否手动滚动 - // LogTextBox.ScrollChanged += LogTextBox_ScrollChanged; - } - - /// - /// 添加日志到缓冲区 - /// - public void AppendText(string text) - { - lock (logBuffer) - { - logBuffer.Append(text); - - // 异步写入日志到文件 - // Task.Run(() => File.AppendAllText("log.txt", text)); - //FlushLog(); - // 如果日志达到阈值,立即刷新 - if (logBuffer.Length > flushThreshold) - { - FlushLog(); - } - } - } - - /// - /// 清空日志缓冲区并更新到 TextBox 中 - /// - private void FlushLog() - { - if (logBuffer.Length == 0) return; - - Dispatcher.InvokeAsync(() => - { - lock (logBuffer) - { - // 仅追加部分日志,避免一次更新过多内容 - string logContent = logBuffer.Length > maxFlushSize - ? logBuffer.ToString(0, maxFlushSize) - : logBuffer.ToString(); - logBuffer.Remove(0, logContent.Length); // 清空已更新的部分 - - LogTextBox.Dispatcher.Invoke(() => - { - LogTextBox.AppendText(logContent); - }); - - } - - // 不必每次都修剪日志,当行数超过限制20%时再修剪 - if (LogTextBox.LineCount > MaxLines * 1.2) - { - TrimLog(); - } - - ScrollToEndIfNeeded(); // 根据是否需要自动滚动来决定 - }, System.Windows.Threading.DispatcherPriority.Background); - } - - /// - /// 限制日志输出的最大行数,超出时删除旧日志 - /// - private void TrimLog() - { - if (LogTextBox.LineCount > MaxLines) - { - // 删除最早的多余行 - LogTextBox.Text = LogTextBox.Text.Substring( - LogTextBox.GetCharacterIndexFromLineIndex(LogTextBox.LineCount - MaxLines)); - } - } - - /// - /// 检测用户是否手动滚动了文本框 - /// - private void LogTextBox_ScrollChanged(object sender, System.Windows.Controls.ScrollChangedEventArgs e) - { - if (e.ExtentHeightChange == 0) // 用户手动滚动时 - { - // 判断是否滚动到底部 - //autoScroll = LogTextBox.VerticalOffset == LogTextBox.ScrollableHeight; - } - } - - /// - /// 根据 autoScroll 标志决定是否滚动到末尾 - /// - private void ScrollToEndIfNeeded() - { - if (autoScroll) - { - LogTextBox.ScrollToEnd(); // 仅在需要时滚动到末尾 - } - } - - /// - /// 清空日志 - /// - public void Clear() - { - Dispatcher.BeginInvoke(() => - { - LogTextBox.Clear(); - }); - } - - /// - /// 点击清空日志按钮时触发 - /// - private void ClearLog_Click(object sender, RoutedEventArgs e) - { - LogTextBox.Clear(); - } - - /// - /// 窗口关闭事件,隐藏窗体而不是关闭 - /// - private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) - { - logBuffer?.Clear(); - Clear(); - e.Cancel = true; // 取消关闭操作 - this.Hide(); // 隐藏窗体而不是关闭 - } - } - -} diff --git a/WorkBench/MainWindow.xaml b/WorkBench/MainWindow.xaml deleted file mode 100644 index 87cef37..0000000 --- a/WorkBench/MainWindow.xaml +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WorkBench/MainWindow.xaml.cs b/WorkBench/MainWindow.xaml.cs deleted file mode 100644 index f8c3a5a..0000000 --- a/WorkBench/MainWindow.xaml.cs +++ /dev/null @@ -1,3027 +0,0 @@ -using Microsoft.Win32; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Serein.Library; -using Serein.Library.Api; -using Serein.Library.Utils; -using Serein.NodeFlow; -using Serein.NodeFlow.Tool; -using Serein.Workbench.Extension; -using Serein.Workbench.Node; -using Serein.Workbench.Node.View; -using Serein.Workbench.Node.ViewModel; -using Serein.Workbench.Themes; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Animation; -using DataObject = System.Windows.DataObject; - -namespace Serein.Workbench -{ - /// - /// 拖拽创建节点类型 - /// - public static class MouseNodeType - { - /// - /// 创建来自DLL的节点 - /// - public static string CreateDllNodeInCanvas { get; } = nameof(CreateDllNodeInCanvas); - /// - /// 创建基础节点 - /// - public static string CreateBaseNodeInCanvas { get; } = nameof(CreateBaseNodeInCanvas); - } - - - - /// - /// Interaction logic for MainWindow.xaml,第一次用git,不太懂 - /// - public partial class MainWindow : Window - { - /// - /// 全局捕获Console输出事件,打印在这个窗体里面 - /// - private readonly LogWindow LogOutWindow = new LogWindow(); - - /// - /// 流程环境装饰器,方便在本地与远程环境下切换 - /// - private IFlowEnvironment EnvDecorator => ViewModel.FlowEnvironment; - private IFlowEnvironmentEvent EnvEventDecorator => ViewModel.FlowEnvironment as IFlowEnvironmentEvent; - private MainWindowViewModel ViewModel { get; set; } - - - /// - /// 节点对应的控件类型 - /// - // private Dictionary NodeUITypes { get; } = []; - - /// - /// 存储所有与节点有关的控件 - /// 任何情景下都应避免直接操作 ViewModel 中的 NodeModel 节点, - /// 而是应该调用 FlowEnvironment 提供接口进行操作, - /// 因为 Workbench 应该更加关注UI视觉效果,而非直接干扰流程环境运行的逻辑。 - /// 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。 - /// - private Dictionary NodeControls { get; } = []; - - /// - /// 存储所有的连接。考虑集成在运行环境中。 - /// - private List Connections { get; } = []; - - /// - /// 起始节点 - /// - //private NodeControlBase StartNodeControl{ get; set; } - - #region 与画布相关的字段 - - /// - /// 标记是否正在尝试选取控件 - /// - private bool IsSelectControl; - /// - /// 标记是否正在进行连接操作 - /// - //private bool IsConnecting; - /// - /// 标记是否正在拖动控件 - /// - private bool IsControlDragging; - /// - /// 标记是否正在拖动画布 - /// - private bool IsCanvasDragging; - private bool IsSelectDragging; - - /// - /// 当前选取的控件 - /// - private readonly List selectNodeControls = []; - - /// - /// 记录开始拖动节点控件时的鼠标位置 - /// - private Point startControlDragPoint; - /// - /// 记录移动画布开始时的鼠标位置 - /// - private Point startCanvasDragPoint; - /// - /// 记录开始选取节点控件时的鼠标位置 - /// - private Point startSelectControolPoint; - - - /// - /// 记录开始连接的文本块 - /// - //private NodeControlBase? startConnectNodeControl; - /// - /// 当前正在绘制的连接线 - /// - //private Line? currentLine; - /// - /// 当前正在绘制的真假分支属性 - /// - //private ConnectionInvokeType currentConnectionType; - - - /// - /// 组合变换容器 - /// - private readonly TransformGroup canvasTransformGroup; - /// - /// 缩放画布 - /// - private readonly ScaleTransform scaleTransform; - /// - /// 平移画布 - /// - private readonly TranslateTransform translateTransform; - #endregion - - - public MainWindow() - { - ViewModel = new MainWindowViewModel(this); - this.DataContext = ViewModel; - InitializeComponent(); - - ViewObjectViewer.FlowEnvironment = EnvDecorator; // 设置 节点树视图 的环境为装饰器 - IOCObjectViewer.FlowEnvironment = EnvDecorator; // 设置 IOC容器视图 的环境为装饰器 - IOCObjectViewer.SelectObj += ViewObjectViewer.LoadObjectInformation; // 使选择 IOC容器视图 的某项(对象)时,可以在 数据视图 呈现数据 - - #region 为 NodeControlType 枚举 不同项添加对应的 Control类型 、 ViewModel类型 - NodeMVVMManagement.RegisterUI(NodeControlType.Action, typeof(ActionNodeControl), typeof(ActionNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.Flipflop, typeof(FlipflopNodeControl), typeof(FlipflopNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.ExpOp, typeof(ExpOpNodeControl), typeof(ExpOpNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.ExpCondition, typeof(ConditionNodeControl), typeof(ConditionNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.ConditionRegion, typeof(ConditionRegionControl), typeof(ConditionRegionNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.GlobalData, typeof(GlobalDataControl), typeof(GlobalDataNodeControlViewModel)); - NodeMVVMManagement.RegisterUI(NodeControlType.Script, typeof(ScriptNodeControl), typeof(ScriptNodeControlViewModel)); - #endregion - - - #region 缩放平移容器 - canvasTransformGroup = new TransformGroup(); - scaleTransform = new ScaleTransform(); - translateTransform = new TranslateTransform(); - canvasTransformGroup.Children.Add(scaleTransform); - canvasTransformGroup.Children.Add(translateTransform); - FlowChartCanvas.RenderTransform = canvasTransformGroup; - #endregion - - InitFlowEnvironmentEvent(); // 配置环境事件 - - - } - - - - /// - /// 初始化环境事件 - /// - private void InitFlowEnvironmentEvent() - { - EnvEventDecorator.OnDllLoad += FlowEnvironment_DllLoadEvent; - EnvEventDecorator.OnProjectSaving += EnvDecorator_OnProjectSaving; - EnvEventDecorator.OnProjectLoaded += FlowEnvironment_OnProjectLoaded; - EnvEventDecorator.OnStartNodeChange += FlowEnvironment_StartNodeChangeEvent; - EnvEventDecorator.OnNodeConnectChange += FlowEnvironment_NodeConnectChangeEvemt; - EnvEventDecorator.OnNodeCreate += FlowEnvironment_NodeCreateEvent; - EnvEventDecorator.OnNodeRemove += FlowEnvironment_NodeRemoveEvent; - EnvEventDecorator.OnNodePlace += EnvDecorator_OnNodePlaceEvent; - EnvEventDecorator.OnNodeTakeOut += EnvDecorator_OnNodeTakeOutEvent; - EnvEventDecorator.OnFlowRunComplete += FlowEnvironment_OnFlowRunCompleteEvent; - - - EnvEventDecorator.OnMonitorObjectChange += FlowEnvironment_OnMonitorObjectChangeEvent; - EnvEventDecorator.OnNodeInterruptStateChange += FlowEnvironment_OnNodeInterruptStateChangeEvent; - EnvEventDecorator.OnInterruptTrigger += FlowEnvironment_OnInterruptTriggerEvent; - - EnvEventDecorator.OnIOCMembersChanged += FlowEnvironment_OnIOCMembersChangedEvent; - - EnvEventDecorator.OnNodeLocated += FlowEnvironment_OnNodeLocateEvent; - EnvEventDecorator.OnNodeMoved += FlowEnvironment_OnNodeMovedEvent; - EnvEventDecorator.OnEnvOut += FlowEnvironment_OnEnvOutEvent; - } - - - - /// - /// 移除环境事件 - /// - private void ResetFlowEnvironmentEvent() - { - EnvEventDecorator.OnDllLoad -= FlowEnvironment_DllLoadEvent; - EnvEventDecorator.OnProjectSaving -= EnvDecorator_OnProjectSaving; - EnvEventDecorator.OnProjectLoaded -= FlowEnvironment_OnProjectLoaded; - EnvEventDecorator.OnStartNodeChange -= FlowEnvironment_StartNodeChangeEvent; - EnvEventDecorator.OnNodeConnectChange -= FlowEnvironment_NodeConnectChangeEvemt; - EnvEventDecorator.OnNodeCreate -= FlowEnvironment_NodeCreateEvent; - EnvEventDecorator.OnNodeRemove -= FlowEnvironment_NodeRemoveEvent; - EnvEventDecorator.OnNodePlace -= EnvDecorator_OnNodePlaceEvent; - EnvEventDecorator.OnNodeTakeOut -= EnvDecorator_OnNodeTakeOutEvent; - EnvEventDecorator.OnFlowRunComplete -= FlowEnvironment_OnFlowRunCompleteEvent; - - - EnvEventDecorator.OnMonitorObjectChange -= FlowEnvironment_OnMonitorObjectChangeEvent; - EnvEventDecorator.OnNodeInterruptStateChange -= FlowEnvironment_OnNodeInterruptStateChangeEvent; - EnvEventDecorator.OnInterruptTrigger -= FlowEnvironment_OnInterruptTriggerEvent; - - EnvEventDecorator.OnIOCMembersChanged -= FlowEnvironment_OnIOCMembersChangedEvent; - EnvEventDecorator.OnNodeLocated -= FlowEnvironment_OnNodeLocateEvent; - EnvEventDecorator.OnNodeMoved -= FlowEnvironment_OnNodeMovedEvent; - - EnvEventDecorator.OnEnvOut -= FlowEnvironment_OnEnvOutEvent; - - } - - #region 窗体加载方法 - private async void Window_Loaded(object sender, RoutedEventArgs e) - { - var currentPath = System.IO.Directory.GetCurrentDirectory(); // 当前目录 - var baseLibraryFilePath = Path.Combine(currentPath, FlowLibraryManagement.SereinBaseLibrary); - if (File.Exists(baseLibraryFilePath)) - { - EnvDecorator.LoadLibrary(baseLibraryFilePath); // 默认加载 - } - - if (App.FlowProjectData is not null) - { - try - { - await Task.Run(() => - { - EnvDecorator.LoadProject(new FlowEnvInfo { Project = App.FlowProjectData }, App.FileDataPath); // 加载项目 - }); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - - // - } - private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) - { - LogOutWindow.Close(); - System.Windows.Application.Current.Shutdown(); - } - private void Window_ContentRendered(object sender, EventArgs e) - { - SereinEnv.WriteLine(InfoType.INFO, "load project..."); - var project = App.FlowProjectData; - if (project is null) - { - return; - } - InitializeCanvas(project.Basic.Canvas.Width, project.Basic.Canvas.Height);// 设置画布大小 - //foreach (var connection in Connections) - //{ - // connection.RefreshLine(); // 窗体完成加载后试图刷新所有连接线 - //} - SereinEnv.WriteLine(InfoType.INFO, $"运行环境当前工作目录:{System.IO.Directory.GetCurrentDirectory()}"); - - var canvasData = project.Basic.Canvas; - if (canvasData is not null) - { - scaleTransform.ScaleX = 1; - scaleTransform.ScaleY = 1; - translateTransform.X = 0; - translateTransform.Y = 0; - scaleTransform.ScaleX = canvasData.ScaleX; - scaleTransform.ScaleY = canvasData.ScaleY; - translateTransform.X += canvasData.ViewX; - translateTransform.Y += canvasData.ViewY; - // 应用变换组 - FlowChartCanvas.RenderTransform = canvasTransformGroup; - } - - - } - - - - - #endregion - - #region 运行环境事件 - - /// - /// 环境内容输出 - /// - /// - /// - private void FlowEnvironment_OnEnvOutEvent(InfoType type, string value) - { - LogOutWindow.AppendText($"{DateTime.Now} [{type}] : {value}{Environment.NewLine}"); - } - - /// - /// 需要保存项目 - /// - /// - /// - private void EnvDecorator_OnProjectSaving(ProjectSavingEventArgs eventArgs) - { - SereinProjectData projectData; - try - { - projectData = EnvDecorator.GetProjectInfoAsync() - .GetAwaiter().GetResult(); // 保存项目 - - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - - projectData.Basic = new Basic - { - Canvas = new FlowCanvas - { - Height = FlowChartCanvas.Height, - Width = FlowChartCanvas.Width, - ViewX = translateTransform.X, - ViewY = translateTransform.Y, - ScaleX = scaleTransform.ScaleX, - ScaleY = scaleTransform.ScaleY, - }, - Versions = "1", - }; - - // 创建一个新的保存文件对话框 - SaveFileDialog saveFileDialog = new() - { - Filter = "DynamicNodeFlow Files (*.dnf)|*.dnf", - DefaultExt = "dnf", - FileName = "project.dnf" - // FileName = System.IO.Path.GetFileName(App.FileDataPath) - }; - - // 显示保存文件对话框 - bool? result = saveFileDialog.ShowDialog(); - // 如果用户选择了文件并点击了保存按钮 - if (result == false) - { - SereinEnv.WriteLine(InfoType.ERROR, "取消保存文件"); - return; - } - - var savePath = saveFileDialog.FileName; - string? librarySavePath = System.IO.Path.GetDirectoryName(savePath); - if (string.IsNullOrEmpty(librarySavePath)) - { - SereinEnv.WriteLine(InfoType.ERROR, "保存项目DLL时返回了意外的文件保存路径"); - return; - } - - - Uri saveProjectFileUri = new Uri(savePath); - SereinEnv.WriteLine(InfoType.INFO, "项目文件保存路径:" + savePath); - for (int index = 0; index < projectData.Librarys.Length; index++) - { - NodeLibraryInfo? library = projectData.Librarys[index]; - string sourceFilePath = new Uri(library.FilePath).LocalPath; // 源文件夹 - string targetFilePath = System.IO.Path.Combine(librarySavePath, library.FileName); // 目标文件夹 - - try - { - if (File.Exists(sourceFilePath)) - { - if (!File.Exists(targetFilePath)) - { - SereinEnv.WriteLine(InfoType.INFO, $"源文件路径 : {sourceFilePath}"); - SereinEnv.WriteLine(InfoType.INFO, $"目标路径 : {targetFilePath}"); - File.Copy(sourceFilePath, targetFilePath, true); - - } - else - { - SereinEnv.WriteLine(InfoType.WARN, $"目标路径已有类库文件: {targetFilePath}"); - } - } - else - { - SereinEnv.WriteLine(InfoType.WARN, $"源文件不存在 : {targetFilePath}"); - } - } - catch (IOException ex) - { - - SereinEnv.WriteLine(InfoType.ERROR, ex.Message); - } - var dirName = System.IO.Path.GetDirectoryName(targetFilePath); - if (!string.IsNullOrEmpty(dirName)) - { - var tmpUri2 = new Uri(targetFilePath); - var relativePath = saveProjectFileUri.MakeRelativeUri(tmpUri2).ToString(); // 转为类库的相对文件路径 - - - - - //string relativePath = System.IO.Path.GetRelativePath(savePath, targetPath); - projectData.Librarys[index].FilePath = relativePath; - } - - } - - JObject projectJsonData = JObject.FromObject(projectData); - File.WriteAllText(savePath, projectJsonData.ToString()); - - - } - - /// - /// 加载完成 - /// - /// - private void FlowEnvironment_OnProjectLoaded(ProjectLoadedEventArgs eventArgs) - { - } - - /// - /// 运行完成 - /// - /// - /// - private void FlowEnvironment_OnFlowRunCompleteEvent(FlowEventArgs eventArgs) - { - SereinEnv.WriteLine(InfoType.INFO, "-------运行完成---------\r\n"); - this.Dispatcher.Invoke(() => - { - IOCObjectViewer.ClearObjItem(); - }); - } - - /// - /// 加载了DLL文件,dll内容 - /// - private void FlowEnvironment_DllLoadEvent(LoadDllEventArgs eventArgs) - { - NodeLibraryInfo nodeLibraryInfo = eventArgs.NodeLibraryInfo; - List methodDetailss = eventArgs.MethodDetailss; - - var dllControl = new DllControl(nodeLibraryInfo); - - foreach (var methodDetailsInfo in methodDetailss) - { - if (!EnumHelper.TryConvertEnum(methodDetailsInfo.NodeType, out var nodeType)) - { - continue; - } - switch (nodeType) - { - case Library.NodeType.Action: - dllControl.AddAction(methodDetailsInfo); // 添加动作类型到控件 - break; - case Library.NodeType.Flipflop: - dllControl.AddFlipflop(methodDetailsInfo); // 添加触发器方法到控件 - break; - } - - } - var menu = new ContextMenu(); - menu.Items.Add(CreateMenuItem("卸载", (s, e) => - { - if (this.EnvDecorator.TryUnloadLibrary(nodeLibraryInfo.AssemblyName)) - { - DllStackPanel.Children.Remove(dllControl); - } - else - { - SereinEnv.WriteLine(InfoType.INFO, "卸载失败"); - } - })); - - dllControl.ContextMenu = menu; - - DllStackPanel.Children.Add(dllControl); // 将控件添加到界面上显示 - - } - - /// - /// 节点连接关系变更 - /// - /// - private void FlowEnvironment_NodeConnectChangeEvemt(NodeConnectChangeEventArgs eventArgs) - { - string fromNodeGuid = eventArgs.FromNodeGuid; - string toNodeGuid = eventArgs.ToNodeGuid; - if (!TryGetControl(fromNodeGuid, out var fromNodeControl) - || !TryGetControl(toNodeGuid, out var toNodeControl)) - { - return; - } - - if (eventArgs.JunctionOfConnectionType == JunctionOfConnectionType.Invoke) - { - ConnectionInvokeType connectionType = eventArgs.ConnectionInvokeType; - #region 创建/删除节点之间的调用关系 - #region 创建连接 - if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 - { - if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) - { - SereinEnv.WriteLine(InfoType.INFO, "非预期的连接"); - return; - } - JunctionControlBase startJunction = IFormJunction.NextStepJunction; - JunctionControlBase endJunction = IToJunction.ExecuteJunction; - - // 添加连接 - var connection = new ConnectionControl( - FlowChartCanvas, - connectionType, - startJunction, - endJunction - ); - - if (toNodeControl is FlipflopNodeControl flipflopControl - && flipflopControl?.ViewModel?.NodeModel is NodeModelBase nodeModel) // 某个节点连接到了触发器,尝试从全局触发器视图中移除该触发器 - { - NodeTreeViewer.RemoveGlobalFlipFlop(nodeModel); // 从全局触发器树树视图中移除 - } - Connections.Add(connection); - fromNodeControl.AddCnnection(connection); - toNodeControl.AddCnnection(connection); - EndConnection(); // 环境触发了创建节点连接事件 - - } - #endregion - #region 移除连接 - else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 - { - // 需要移除连接 - var removeConnections = Connections.Where(c => - c.Start.MyNode.Guid.Equals(fromNodeGuid) - && c.End.MyNode.Guid.Equals(toNodeGuid) - && (c.Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke - || c.End.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke)) - .ToList(); - - - foreach (var connection in removeConnections) - { - Connections.Remove(connection); - fromNodeControl.RemoveConnection(connection); // 移除连接 - toNodeControl.RemoveConnection(connection); // 移除连接 - if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) - { - JudgmentFlipFlopNode(control); // 连接关系变更时判断 - } - } - } - #endregion - #endregion - } - else - { - ConnectionArgSourceType connectionArgSourceType = eventArgs.ConnectionArgSourceType; - #region 创建/删除节点之间的参数传递关系 - #region 创建连接 - if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 - { - if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) - { - SereinEnv.WriteLine(InfoType.INFO, "非预期的情况"); - return; - } - - JunctionControlBase startJunction = eventArgs.ConnectionArgSourceType switch - { - ConnectionArgSourceType.GetPreviousNodeData => IFormJunction.ReturnDataJunction, // 自身节点 - ConnectionArgSourceType.GetOtherNodeData => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 - ConnectionArgSourceType.GetOtherNodeDataOfInvoke => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 - _ => throw new Exception("窗体事件 FlowEnvironment_NodeConnectChangeEvemt 创建/删除节点之间的参数传递关系 JunctionControlBase 枚举值错误 。非预期的枚举值。") // 应该不会触发 - }; - - if(IToJunction.ArgDataJunction.Length <= eventArgs.ArgIndex) - { - _ = Task.Run(async () => - { - await Task.Delay(500); - FlowEnvironment_NodeConnectChangeEvemt(eventArgs); - }); - return; - } - JunctionControlBase endJunction = IToJunction.ArgDataJunction[eventArgs.ArgIndex]; - LineType lineType = LineType.Bezier; - // 添加连接 - var connection = new ConnectionControl( - lineType, - FlowChartCanvas, - eventArgs.ArgIndex, - eventArgs.ConnectionArgSourceType, - startJunction, - endJunction, - IToJunction - ); - Connections.Add(connection); - fromNodeControl.AddCnnection(connection); - toNodeControl.AddCnnection(connection); - EndConnection(); // 环境触发了创建节点连接事件 - - - } - #endregion - #region 移除连接 - else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 - { - // 需要移除连接 - var removeConnections = Connections.Where(c => c.Start.MyNode.Guid.Equals(fromNodeGuid) - && c.End.MyNode.Guid.Equals(toNodeGuid)) - .ToList(); // 获取这两个节点之间的所有连接关系 - - - - foreach (var connection in removeConnections) - { - if (connection.End is ArgJunctionControl junctionControl && junctionControl.ArgIndex == eventArgs.ArgIndex) - { - // 找到符合删除条件的连接线 - Connections.Remove(connection); // 从本地记录中移除 - fromNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 - toNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 - } - - - //if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) - //{ - // JudgmentFlipFlopNode(control); // 连接关系变更时判断 - //} - } - } - #endregion - #endregion - } - } - - /// - /// 节点移除事件 - /// - /// - private void FlowEnvironment_NodeRemoveEvent(NodeRemoveEventArgs eventArgs) - { - var nodeGuid = eventArgs.NodeGuid; - if (!TryGetControl(nodeGuid, out var nodeControl)) - { - return; - } - - if (nodeControl is null) return; - if (selectNodeControls.Count > 0) - { - if (selectNodeControls.Contains(nodeControl)) - { - selectNodeControls.Remove(nodeControl); - } - } - - if (nodeControl is FlipflopNodeControl flipflopControl) // 判断是否为触发器 - { - var node = flipflopControl?.ViewModel?.NodeModel; - if (node is not null) - { - NodeTreeViewer.RemoveGlobalFlipFlop(node); // 从全局触发器树树视图中移除 - } - } - - - - FlowChartCanvas.Children.Remove(nodeControl); - nodeControl.RemoveAllConection(); - NodeControls.Remove(nodeControl.ViewModel.NodeModel.Guid); - } - - /// - /// 添加节点事件 - /// - /// 添加节点事件参数 - /// - private void FlowEnvironment_NodeCreateEvent(NodeCreateEventArgs eventArgs) - { - var nodeModel = eventArgs.NodeModel; - if (NodeControls.ContainsKey(nodeModel.Guid)) - { - SereinEnv.WriteLine(InfoType.WARN, $"OnNodeCreateEvent 事件接收到意外的返回值:节点Guid重复 - {nodeModel.Guid}"); - return; - } - - PositionOfUI position = eventArgs.Position; - - if(!NodeMVVMManagement.TryGetType(nodeModel.ControlType, out var nodeMVVM)) - { - SereinEnv.WriteLine(InfoType.INFO, $"无法创建{nodeModel.ControlType}节点,节点类型尚未注册。"); - return; - } - if(nodeMVVM.ControlType == null - || nodeMVVM.ViewModelType == null) - { - SereinEnv.WriteLine(InfoType.INFO, $"无法创建{nodeModel.ControlType}节点,UI类型尚未注册(请通过 NodeMVVMManagement.RegisterUI() 方法进行注册)。"); - return; - } - - var nodeCanvas = FlowChartCanvas; - NodeControlBase nodeControl; - try - { - nodeControl = CreateNodeControl(nodeMVVM.ControlType, // 控件UI类型 - nodeMVVM.ViewModelType, // 控件VIewModel类型 - nodeModel, // 控件数据实体 - nodeCanvas); // 所在画布 - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - - NodeControls.TryAdd(nodeModel.Guid, nodeControl); // 添加到 - if (TryPlaceNodeInRegion(nodeControl, position, out var regionControl)) // 判断添加到区域容器 - { - // 通知运行环境调用加载节点子项的方法 - _ = EnvDecorator.PlaceNodeToContainerAsync(nodeControl.ViewModel.NodeModel.Guid, // 待移动的节点 - regionControl.ViewModel.NodeModel.Guid); // 目标的容器节点 - } - else - { - // 并非添加在容器中,直接放置节点 - PlaceNodeOnCanvas(nodeControl, position.X, position.Y); - } - - - #region 节点树视图 - if (nodeModel.ControlType == NodeControlType.Flipflop) - { - var node = nodeControl?.ViewModel?.NodeModel; - if (node is not null) - { - NodeTreeViewer.AddGlobalFlipFlop(EnvDecorator, node); // 新增的触发器节点添加到全局触发器 - } - } - - GC.Collect(); - #endregion - - } - - /// - /// 放置一个节点 - /// - /// - /// - private void EnvDecorator_OnNodePlaceEvent(NodePlaceEventArgs eventArgs) - { - string nodeGuid = eventArgs.NodeGuid; - string containerNodeGuid = eventArgs.ContainerNodeGuid; - if (!TryGetControl(nodeGuid, out var nodeControl) - || !TryGetControl(containerNodeGuid, out var containerNodeControl)) - { - return; - } - if(containerNodeControl is not INodeContainerControl containerControl) - { - SereinEnv.WriteLine(InfoType.WARN, - $"节点[{nodeGuid}]无法放置于节点[{containerNodeGuid}]," + - $"因为后者并不实现 INodeContainerControl 接口"); - return; - } - nodeControl.PlaceToContainer(containerControl); // 放置在容器节点中 - } - - /// - /// 取出一个节点 - /// - /// - private void EnvDecorator_OnNodeTakeOutEvent(NodeTakeOutEventArgs eventArgs) - { - string nodeGuid = eventArgs.NodeGuid; - if (!TryGetControl(nodeGuid, out var nodeControl)) - { - return; - } - nodeControl.TakeOutContainer(); // 从容器节点中取出 - - } - - - - /// - /// 设置了流程起始控件 - /// - /// - /// - private void FlowEnvironment_StartNodeChangeEvent(StartNodeChangeEventArgs eventArgs) - { - string oldNodeGuid = eventArgs.OldNodeGuid; - string newNodeGuid = eventArgs.NewNodeGuid; - if (!TryGetControl(newNodeGuid, out var newStartNodeControl)) return; - if (!string.IsNullOrEmpty(oldNodeGuid)) - { - if (!TryGetControl(oldNodeGuid, out var oldStartNodeControl)) return; - oldStartNodeControl.BorderBrush = Brushes.Black; - oldStartNodeControl.BorderThickness = new Thickness(0); - } - - newStartNodeControl.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")); - newStartNodeControl.BorderThickness = new Thickness(2); - var node = newStartNodeControl?.ViewModel?.NodeModel; - if (node is not null) - { - NodeTreeViewer.LoadNodeTreeOfStartNode(EnvDecorator, node); - } - - } - - /// - /// 被监视的对象发生改变 - /// - /// - private void FlowEnvironment_OnMonitorObjectChangeEvent(MonitorObjectEventArgs eventArgs) - { - string nodeGuid = eventArgs.NodeGuid; - - string monitorKey = MonitorObjectEventArgs.ObjSourceType.NodeFlowData switch - { - MonitorObjectEventArgs.ObjSourceType.NodeFlowData => nodeGuid, - _ => eventArgs.NewData.GetType().FullName, - }; - - //NodeControlBase nodeControl = GuidToControl(nodeGuid); - if (ViewObjectViewer.MonitorObj is null) // 如果没有加载过对象 - { - ViewObjectViewer.LoadObjectInformation(monitorKey, eventArgs.NewData); // 加载对象 ViewObjectViewerControl.MonitorType.Obj - } - else - { - if (monitorKey.Equals(ViewObjectViewer.MonitorKey)) // 相同对象 - { - ViewObjectViewer.RefreshObjectTree(eventArgs.NewData); // 刷新 - } - else - { - ViewObjectViewer.LoadObjectInformation(monitorKey, eventArgs.NewData); // 加载对象 - } - } - - } - - /// - /// 节点中断状态改变。 - /// - /// - private void FlowEnvironment_OnNodeInterruptStateChangeEvent(NodeInterruptStateChangeEventArgs eventArgs) - { - string nodeGuid = eventArgs.NodeGuid; - if (!TryGetControl(nodeGuid, out var nodeControl)) return; - - //if (eventArgs.Class == InterruptClass.None) - //{ - // nodeControl.ViewModel.IsInterrupt = false; - //} - //else - //{ - // nodeControl.ViewModel.IsInterrupt = true; - //} - if(nodeControl.ContextMenu == null) - { - return; - } - foreach (var menuItem in nodeControl.ContextMenu.Items) - { - if (menuItem is MenuItem menu) - { - if ("取消中断".Equals(menu.Header)) - { - menu.Header = "在此中断"; - } - else if ("在此中断".Equals(menu.Header)) - { - menu.Header = "取消中断"; - } - - } - } - - } - - /// - /// 节点触发了中断 - /// - /// - /// - private void FlowEnvironment_OnInterruptTriggerEvent(InterruptTriggerEventArgs eventArgs) - { - string nodeGuid = eventArgs.NodeGuid; - if (!TryGetControl(nodeGuid, out var nodeControl)) return; - if(eventArgs.Type == InterruptTriggerEventArgs.InterruptTriggerType.Exp) - { - SereinEnv.WriteLine(InfoType.INFO, $"表达式触发了中断:{eventArgs.Expression}"); - } - else - { - SereinEnv.WriteLine(InfoType.INFO, $"节点触发了中断:{nodeGuid}"); - } - } - - /// - /// IOC变更 - /// - /// - /// - private void FlowEnvironment_OnIOCMembersChangedEvent(IOCMembersChangedEventArgs eventArgs) - { - IOCObjectViewer.AddDependenciesInstance(eventArgs.Key, eventArgs.Instance); - - } - - /// - /// 节点需要定位 - /// - /// - /// - private void FlowEnvironment_OnNodeLocateEvent(NodeLocatedEventArgs eventArgs) - { - if (!TryGetControl(eventArgs.NodeGuid, out var nodeControl)) return; - //scaleTransform.ScaleX = 1; - //scaleTransform.ScaleY = 1; - // 获取控件在 FlowChartCanvas 上的相对位置 - Rect controlBounds = VisualTreeHelper.GetDescendantBounds(nodeControl); - Point controlPosition = nodeControl.TransformToAncestor(FlowChartCanvas).Transform(new Point(0, 0)); - - // 获取控件在画布上的中心点 - double controlCenterX = controlPosition.X + controlBounds.Width / 2; - double controlCenterY = controlPosition.Y + controlBounds.Height / 2; - - // 考虑缩放因素计算目标位置的中心点 - double scaledCenterX = controlCenterX * scaleTransform.ScaleX; - double scaledCenterY = controlCenterY * scaleTransform.ScaleY; - - - //// 计算画布的可视区域大小 - //double visibleAreaLeft = scaledCenterX; - //double visibleAreaTop = scaledCenterY; - //double visibleAreaRight = scaledCenterX + FlowChartStackGrid.ActualWidth; - //double visibleAreaBottom = scaledCenterY + FlowChartStackGrid.ActualHeight; - //// 检查控件中心点是否在可视区域内 - //bool isInView = scaledCenterX >= visibleAreaLeft && scaledCenterX <= visibleAreaRight && - // scaledCenterY >= visibleAreaTop && scaledCenterY <= visibleAreaBottom; - - //Console.WriteLine($"isInView :{isInView}"); - - //if (!isInView) - //{ - //} - // 计算平移偏移量,使得控件在可视区域的中心 - double translateX = scaledCenterX - FlowChartStackGrid.ActualWidth / 2; - double translateY = scaledCenterY - FlowChartStackGrid.ActualHeight / 2; - - var translate = this.translateTransform; - // 应用平移变换 - translate.X = 0; - translate.Y = 0; - translate.X -= translateX; - translate.Y -= translateY; - - // 设置RenderTransform以实现移动效果 - TranslateTransform translateTransform = new TranslateTransform(); - nodeControl.RenderTransform = translateTransform; - ElasticAnimation(nodeControl, translateTransform, 4, 1, 0.5); - - } - - /// - /// 控件抖动 - /// 来源:https://www.cnblogs.com/RedSky/p/17705411.html - /// 作者:HotSky - /// (……太好用了) - /// - /// - /// 需要抖动的控件 - /// 抖动第一下偏移量 - /// 减弱幅度(小于等于power,大于0) - /// 持续系数(大于0),越大时间越长, - private static void ElasticAnimation(NodeControlBase nodeControl, TranslateTransform translate, int power, int range = 1, double speed = 1) - { - DoubleAnimationUsingKeyFrames animation1 = new DoubleAnimationUsingKeyFrames(); - for (int i = power, j = 1; i >= 0; i -= range) - { - animation1.KeyFrames.Add(new LinearDoubleKeyFrame(-i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); - animation1.KeyFrames.Add(new LinearDoubleKeyFrame(i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); - } - translate.BeginAnimation(TranslateTransform.YProperty, animation1); - DoubleAnimationUsingKeyFrames animation2 = new DoubleAnimationUsingKeyFrames(); - for (int i = power, j = 1; i >= 0; i -= range) - { - animation2.KeyFrames.Add(new LinearDoubleKeyFrame(-i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); - animation2.KeyFrames.Add(new LinearDoubleKeyFrame(i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); - } - translate.BeginAnimation(TranslateTransform.XProperty, animation2); - - animation2.Completed += (s, e) => - { - nodeControl.RenderTransform = null; // 或者重新设置为默认值 - }; - } - - /// - /// 节点移动 - /// - /// - private void FlowEnvironment_OnNodeMovedEvent(NodeMovedEventArgs eventArgs) - { - if (!TryGetControl(eventArgs.NodeGuid, out var nodeControl)) return; - nodeControl.UpdateLocationConnections(); - - //var newLeft = eventArgs.X; - //var newTop = eventArgs.Y; - //// 限制控件不超出FlowChartCanvas的边界 - //if (newLeft >= 0 && newLeft + nodeControl.ActualWidth <= FlowChartCanvas.ActualWidth) - //{ - // Canvas.SetLeft(nodeControl, newLeft); - - //} - //if (newTop >= 0 && newTop + nodeControl.ActualHeight <= FlowChartCanvas.ActualHeight) - //{ - // Canvas.SetTop(nodeControl, newTop); - //} - - - } - - /// - /// Guid 转 NodeControl - /// - /// - /// - /// - private bool TryGetControl(string nodeGuid,out NodeControlBase nodeControl) - { - if (string.IsNullOrEmpty(nodeGuid)) - { - nodeControl = null; - return false; - } - if (!NodeControls.TryGetValue(nodeGuid, out nodeControl)) - { - nodeControl = null; - return false; - } - if(nodeControl is null) - { - return false; - } - return true; - } - - #endregion - - - #region 节点控件的创建 - - - /// - /// 创建了节点,添加到画布。配置默认事件 - /// - /// - /// - /// - private void PlaceNodeOnCanvas(NodeControlBase nodeControl, double x, double y) - { - // 添加控件到画布 - FlowChartCanvas.Children.Add(nodeControl); - Canvas.SetLeft(nodeControl, x); - Canvas.SetTop(nodeControl, y); - - ConfigureContextMenu(nodeControl); // 配置节点右键菜单 - ConfigureNodeEvents(nodeControl); // 配置节点事件 - } - - /// - /// 配置节点事件(移动,点击相关) - /// - /// - private void ConfigureNodeEvents(NodeControlBase nodeControl) - { - nodeControl.MouseLeftButtonDown += Block_MouseLeftButtonDown; - nodeControl.MouseMove += Block_MouseMove; - nodeControl.MouseLeftButtonUp += Block_MouseLeftButtonUp; - } - - - #endregion - - #region 配置右键菜单 - - /// - /// 配置节点右键菜单 - /// - /// - /// 任何情景下都尽量避免直接修改 ViewModel 中的 NodeModel 节点实体相关数据。 - /// 而是应该调用 FlowEnvironment 提供接口进行操作。 - /// 因为 Workbench 应该更加关注UI视觉效果,而非直接干扰流程环境运行的逻辑。 - /// 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。 - /// - private void ConfigureContextMenu(NodeControlBase nodeControl) - { - - var contextMenu = new ContextMenu(); - var nodeGuid = nodeControl.ViewModel?.NodeModel?.Guid; - #region 触发器节点 - - if(nodeControl.ViewModel?.NodeModel.ControlType == NodeControlType.Flipflop) - { - contextMenu.Items.Add(CreateMenuItem("启动触发器", (s, e) => - { - if (s is MenuItem menuItem) - { - if (menuItem.Header.ToString() == "启动触发器") - { - EnvDecorator.ActivateFlipflopNode(nodeGuid); - - menuItem.Header = "终结触发器"; - } - else - { - EnvDecorator.TerminateFlipflopNode(nodeGuid); - menuItem.Header = "启动触发器"; - - } - } - })); - } - - #endregion - - if (nodeControl.ViewModel?.NodeModel?.MethodDetails?.ReturnType is Type returnType && returnType != typeof(void)) - { - contextMenu.Items.Add(CreateMenuItem("查看返回类型", (s, e) => - { - DisplayReturnTypeTreeViewer(returnType); - })); - } - - - - contextMenu.Items.Add(CreateMenuItem("设为起点", (s, e) => EnvDecorator.SetStartNodeAsync(nodeGuid))); - contextMenu.Items.Add(CreateMenuItem("删除", async (s, e) => - { - var result = await EnvDecorator.RemoveNodeAsync(nodeGuid); - })); - - #region 右键菜单功能 - 控件对齐 - - var AvoidMenu = new MenuItem(); - AvoidMenu.Items.Add(CreateMenuItem("群组对齐", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.Grouping); - })); - AvoidMenu.Items.Add(CreateMenuItem("规划对齐", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.Planning); - })); - AvoidMenu.Items.Add(CreateMenuItem("水平中心对齐", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.HorizontalCenter); - })); - AvoidMenu.Items.Add(CreateMenuItem("垂直中心对齐 ", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.VerticalCenter); - })); - - AvoidMenu.Items.Add(CreateMenuItem("垂直对齐时水平斜分布", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.Vertical); - })); - AvoidMenu.Items.Add(CreateMenuItem("水平对齐时垂直斜分布", (s, e) => - { - AlignControlsWithGrouping(selectNodeControls, AlignMode.Horizontal); - })); - - AvoidMenu.Header = "对齐"; - contextMenu.Items.Add(AvoidMenu); - - - #endregion - - nodeControl.ContextMenu = contextMenu; - } - - /// - /// 查看返回类型(树形结构展开类型的成员) - /// - /// - private void DisplayReturnTypeTreeViewer(Type type) - { - try - { - var typeViewerWindow = new TypeViewerWindow - { - Type = type, - }; - typeViewerWindow.LoadTypeInformation(); - typeViewerWindow.Show(); - } - catch (Exception ex) - { - SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); - } - } - #endregion - - #region 拖拽DLL文件到左侧功能区,加载相关节点清单 - /// - /// 当拖动文件到窗口时触发,加载DLL文件 - /// - /// - /// - private void Window_Drop(object sender, DragEventArgs e) - { - if (e.Data.GetDataPresent(DataFormats.FileDrop)) - { - string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); - foreach (string file in files) - { - if (file.EndsWith(".dll")) - { - try - { - EnvDecorator.LoadLibrary(file); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - } - } - } - - /// - /// 当拖动文件经过窗口时触发,设置拖放效果为复制 - /// - /// - /// - private void Window_DragOver(object sender, DragEventArgs e) - { - e.Effects = DragDropEffects.Copy; - e.Handled = true; - } - - #endregion - - #region 与流程图/节点相关 - - /// - /// 鼠标在画布移动。 - /// 选择控件状态下,调整选择框大小 - /// 连接状态下,实时更新连接线的终点位置。 - /// 移动画布状态下,移动画布。 - /// - private void FlowChartCanvas_MouseMove(object sender, MouseEventArgs e) - { - var myData = GlobalJunctionData.MyGlobalConnectingData; - if (myData.IsCreateing && e.LeftButton == MouseButtonState.Pressed) - { - - if (myData.Type == JunctionOfConnectionType.Invoke) - { - ViewModel.IsConnectionInvokeNode = true; // 正在连接节点的调用关系 - - } - else - { - ViewModel.IsConnectionArgSourceNode = true; // 正在连接节点的调用关系 - } - var currentPoint = e.GetPosition(FlowChartCanvas); - currentPoint.X -= 2; - currentPoint.Y -= 2; - myData.UpdatePoint(currentPoint); - return; - } - - - - if (IsCanvasDragging && e.MiddleButton == MouseButtonState.Pressed) // 正在移动画布(按住中键) - { - Point currentMousePosition = e.GetPosition(this); - double deltaX = currentMousePosition.X - startCanvasDragPoint.X; - double deltaY = currentMousePosition.Y - startCanvasDragPoint.Y; - - translateTransform.X += deltaX; - translateTransform.Y += deltaY; - - startCanvasDragPoint = currentMousePosition; - - foreach (var line in Connections) - { - line.RefreshLine(); // 画布移动时刷新所有连接线 - } - } - - if (IsSelectControl) // 正在选取节点 - { - IsSelectDragging = e.LeftButton == MouseButtonState.Pressed; - // 获取当前鼠标位置 - Point currentPoint = e.GetPosition(FlowChartCanvas); - - // 更新选取矩形的位置和大小 - double x = Math.Min(currentPoint.X, startSelectControolPoint.X); - double y = Math.Min(currentPoint.Y, startSelectControolPoint.Y); - double width = Math.Abs(currentPoint.X - startSelectControolPoint.X); - double height = Math.Abs(currentPoint.Y - startSelectControolPoint.Y); - - Canvas.SetLeft(SelectionRectangle, x); - Canvas.SetTop(SelectionRectangle, y); - SelectionRectangle.Width = width; - SelectionRectangle.Height = height; - - } - } - - /// - /// 基础节点的拖拽放置创建 - /// - /// - /// - private void BaseNodeControl_PreviewMouseMove(object sender, MouseEventArgs e) - { - if (sender is UserControl control) - { - if(e.LeftButton == MouseButtonState.Pressed) - { - // 创建一个 DataObject 用于拖拽操作,并设置拖拽效果 - var dragData = new DataObject(MouseNodeType.CreateBaseNodeInCanvas, control.GetType()); - DragDrop.DoDragDrop(control, dragData, DragDropEffects.Move); - } - - } - } - - /// - /// 放置操作,根据拖放数据创建相应的控件,并处理相关操作 - /// - /// - /// - private void FlowChartCanvas_Drop(object sender, DragEventArgs e) - { - try - { - var canvasDropPosition = e.GetPosition(FlowChartCanvas); // 更新画布落点 - PositionOfUI position = new PositionOfUI(canvasDropPosition.X, canvasDropPosition.Y); - if (e.Data.GetDataPresent(MouseNodeType.CreateDllNodeInCanvas)) - { - if (e.Data.GetData(MouseNodeType.CreateDllNodeInCanvas) is MoveNodeData nodeData) - { - Task.Run(async () => - { - await EnvDecorator.CreateNodeAsync(nodeData.NodeControlType, position, nodeData.MethodDetailsInfo); // 创建DLL文件的节点对象 - }); - } - } - else if (e.Data.GetDataPresent(MouseNodeType.CreateBaseNodeInCanvas)) - { - if (e.Data.GetData(MouseNodeType.CreateBaseNodeInCanvas) is Type droppedType) - { - NodeControlType nodeControlType = droppedType switch - { - Type when typeof(ConditionRegionControl).IsAssignableFrom(droppedType) => NodeControlType.ConditionRegion, // 条件区域 - Type when typeof(ConditionNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpCondition, - Type when typeof(ExpOpNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpOp, - Type when typeof(GlobalDataControl).IsAssignableFrom(droppedType) => NodeControlType.GlobalData, - Type when typeof(ScriptNodeControl).IsAssignableFrom(droppedType) => NodeControlType.Script, - _ => NodeControlType.None, - }; - if (nodeControlType != NodeControlType.None) - { - Task.Run(async () => - { - await EnvDecorator.CreateNodeAsync(nodeControlType, position); // 创建基础节点对象 - }); - } - } - } - e.Handled = true; - } - catch (Exception ex) - { - SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); - } - } - - /// - /// 尝试判断是否为区域,如果是,将节点放置在区域中 - /// - /// - /// - /// 目标节点控件 - /// - private bool TryPlaceNodeInRegion(NodeControlBase nodeControl, - PositionOfUI position, - out NodeControlBase targetNodeControl) - { - var point = new Point(position.X, position.Y); - HitTestResult hitTestResult = VisualTreeHelper.HitTest(FlowChartCanvas, point); - if (hitTestResult != null && hitTestResult.VisualHit is UIElement hitElement) - { - // 准备放置条件表达式控件 - if (nodeControl.ViewModel.NodeModel.ControlType == NodeControlType.ExpCondition) - { - ConditionRegionControl? conditionRegion = GetParentOfType(hitElement); - if (conditionRegion is not null) - { - targetNodeControl = conditionRegion; - //// 如果存在条件区域容器 - //conditionRegion.AddCondition(nodeControl); - return true; - } - } - - else - { - // 准备放置全局数据控件 - GlobalDataControl? globalDataControl = GetParentOfType(hitElement); - if (globalDataControl is not null) - { - targetNodeControl = globalDataControl; - return true; - } - } - } - targetNodeControl = null; - return false; - } - - ///// - ///// 将节点放在目标区域中 - ///// - ///// 区域容器 - ///// 节点控件 - //private void TryPlaceNodeInRegion(NodeControlBase regionControl, NodeControlBase nodeControl) - //{ - // // 准备放置条件表达式控件 - // if (nodeControl.ViewModel.NodeModel.ControlType == NodeControlType.ExpCondition) - // { - // if (regionControl is ConditionRegionControl conditionRegion) - // { - // conditionRegion.AddCondition(nodeControl); // 条件区域容器 - // } - // } - // else if(regionControl.ViewModel.NodeModel.ControlType == NodeControlType.GlobalData) - // { - // if (regionControl is GlobalDataControl globalDataControl) - // { - // // 全局数据节点容器 - // globalDataControl.SetDataNodeControl(nodeControl); - // } - // } - //} - - - - /// - /// 拖动效果,根据拖放数据是否为指定类型设置拖放效果 - /// - /// - /// - private void FlowChartCanvas_DragOver(object sender, DragEventArgs e) - { - if (e.Data.GetDataPresent(MouseNodeType.CreateDllNodeInCanvas) - || e.Data.GetDataPresent(MouseNodeType.CreateBaseNodeInCanvas)) - { - e.Effects = DragDropEffects.Move; - } - else - { - e.Effects = DragDropEffects.None; - } - e.Handled = true; - } - - /// - /// 控件的鼠标左键按下事件,启动拖动操作。同时显示当前正在传递的数据。 - /// - private void Block_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - //if (GlobalJunctionData.IsCreatingConnection) - //{ - // return; - //} - if(sender is NodeControlBase nodeControl) - { - ChangeViewerObjOfNode(nodeControl); - if (nodeControl?.ViewModel?.NodeModel?.MethodDetails?.IsProtectionParameter == true) return; - IsControlDragging = true; - startControlDragPoint = e.GetPosition(FlowChartCanvas); // 记录鼠标按下时的位置 - ((UIElement)sender).CaptureMouse(); // 捕获鼠标 - e.Handled = true; // 防止事件传播影响其他控件 - } - } - - /// - /// 控件的鼠标移动事件,根据鼠标拖动更新控件的位置。批量移动计算移动逻辑。 - /// - private void Block_MouseMove(object sender, MouseEventArgs e) - { - if (IsCanvasDragging) - return; - if (IsSelectControl) - return; - - if (IsControlDragging) // 如果正在拖动控件 - { - Point currentPosition = e.GetPosition(FlowChartCanvas); // 获取当前鼠标位置 - - if (selectNodeControls.Count > 0 && sender is NodeControlBase nodeControlMain && selectNodeControls.Contains(nodeControlMain)) - { - // 进行批量移动 - // 获取旧位置 - var oldLeft = Canvas.GetLeft(nodeControlMain); - var oldTop = Canvas.GetTop(nodeControlMain); - - // 计算被选择控件的偏移量 - var deltaX = /*(int)*/(currentPosition.X - startControlDragPoint.X); - var deltaY = /*(int)*/(currentPosition.Y - startControlDragPoint.Y); - - // 移动被选择的控件 - var newLeft = oldLeft + deltaX; - var newTop = oldTop + deltaY; - - this.EnvDecorator.MoveNode(nodeControlMain.ViewModel.NodeModel.Guid, newLeft, newTop); // 移动节点 - - // 计算控件实际移动的距离 - var actualDeltaX = newLeft - oldLeft; - var actualDeltaY = newTop - oldTop; - - // 移动其它选中的控件 - foreach (var nodeControl in selectNodeControls) - { - if (nodeControl != nodeControlMain) // 跳过已经移动的控件 - { - var otherNewLeft = Canvas.GetLeft(nodeControl) + actualDeltaX; - var otherNewTop = Canvas.GetTop(nodeControl) + actualDeltaY; - this.EnvDecorator.MoveNode(nodeControl.ViewModel.NodeModel.Guid, otherNewLeft, otherNewTop); // 移动节点 - } - } - - // 更新节点之间线的连接位置 - foreach (var nodeControl in selectNodeControls) - { - nodeControl.UpdateLocationConnections(); - } - } - else - { // 单个节点移动 - if (sender is not NodeControlBase nodeControl) - { - return; - } - double deltaX = currentPosition.X - startControlDragPoint.X; // 计算X轴方向的偏移量 - double deltaY = currentPosition.Y - startControlDragPoint.Y; // 计算Y轴方向的偏移量 - double newLeft = Canvas.GetLeft(nodeControl) + deltaX; // 新的左边距 - double newTop = Canvas.GetTop(nodeControl) + deltaY; // 新的上边距 - this.EnvDecorator.MoveNode(nodeControl.ViewModel.NodeModel.Guid, newLeft, newTop); // 移动节点 - nodeControl.UpdateLocationConnections(); - } - startControlDragPoint = currentPosition; // 更新起始点位置 - } - - } - - - // 改变对象树? - private void ChangeViewerObjOfNode(NodeControlBase nodeControl) - { - var node = nodeControl.ViewModel.NodeModel; - //if (node is not null && (node.MethodDetails is null || node.MethodDetails.ReturnType != typeof(void)) - if (node is not null && node.MethodDetails?.ReturnType != typeof(void)) - { - var key = node.Guid; - object instance = null; - //Console.WriteLine("WindowXaml 后台代码中 ChangeViewerObjOfNode 需要重新设计"); - //var instance = node.GetFlowData(); // 对象预览树视图获取(后期更改) - if(instance is not null) - { - ViewObjectViewer.LoadObjectInformation(key, instance); - ChangeViewerObj(key, instance); - } - } - } - public void ChangeViewerObj(string key, object instance) - { - if (ViewObjectViewer.MonitorObj is null) - { - // EnvDecorator.SetMonitorObjState(key, true); // 通知环境,该节点的数据更新后需要传到UI - return; - } - if (instance is null) - { - return; - } - if (key.Equals(ViewObjectViewer.MonitorKey) == true) - { - ViewObjectViewer.RefreshObjectTree(instance); - return; - } - else - { - //EnvDecorator.SetMonitorObjState(ViewObjectViewer.MonitorKey,false); // 取消对旧节点的监视 - //EnvDecorator.SetMonitorObjState(key, true); // 通知环境,该节点的数据更新后需要传到UI - } - } - #endregion - - #region UI连接控件操作 - - /// - /// 控件的鼠标左键松开事件,结束拖动操作 - /// - private void Block_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - if (IsControlDragging) - { - IsControlDragging = false; - ((UIElement)sender).ReleaseMouseCapture(); // 释放鼠标捕获 - - } - - //if (IsConnecting) - //{ - // var formNodeGuid = startConnectNodeControl?.ViewModel.NodeModel.Guid; - // var toNodeGuid = (sender as NodeControlBase)?.ViewModel.NodeModel.Guid; - // if (string.IsNullOrEmpty(formNodeGuid) || string.IsNullOrEmpty(toNodeGuid)) - // { - // return; - // } - // EnvDecorator.ConnectNodeAsync(formNodeGuid, toNodeGuid,0,0, currentConnectionType); - //} - //GlobalJunctionData.OK(); - } - - - /// - /// 结束连接操作,清理状态并移除虚线。 - /// - private void EndConnection() - { - Mouse.OverrideCursor = null; // 恢复视觉效果 - ViewModel.IsConnectionArgSourceNode = false; - ViewModel.IsConnectionInvokeNode = false; - GlobalJunctionData.OK(); - } - - #region 拖动画布实现缩放平移效果 - private void FlowChartCanvas_MouseDown(object sender, MouseButtonEventArgs e) - { - IsCanvasDragging = true; - startCanvasDragPoint = e.GetPosition(this); - FlowChartCanvas.CaptureMouse(); - e.Handled = true; // 防止事件传播影响其他控件 - } - - private void FlowChartCanvas_MouseUp(object sender, MouseButtonEventArgs e) - { - - - - if (IsCanvasDragging) - { - IsCanvasDragging = false; - FlowChartCanvas.ReleaseMouseCapture(); - } - } - - // 单纯缩放画布,不改变画布大小 - private void FlowChartCanvas_MouseWheel(object sender, MouseWheelEventArgs e) - { - // if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) - { - if (e.Delta < 0 && scaleTransform.ScaleX < 0.05) return; - if (e.Delta > 0 && scaleTransform.ScaleY > 2.0) return; - // 获取鼠标在 Canvas 内的相对位置 - var mousePosition = e.GetPosition(FlowChartCanvas); - - // 缩放因子,根据滚轮方向调整 - //double zoomFactor = e.Delta > 0 ? 0.1 : -0.1; - double zoomFactor = e.Delta > 0 ? 1.1 : 0.9; - - // 当前缩放比例 - double oldScale = scaleTransform.ScaleX; - double newScale = oldScale * zoomFactor; - //double newScale = oldScale + zoomFactor; - // 更新缩放比例 - scaleTransform.ScaleX = newScale; - scaleTransform.ScaleY = newScale; - - // 计算缩放前后鼠标相对于 Canvas 的位置差异 - // double offsetX = mousePosition.X - (mousePosition.X * zoomFactor); - // double offsetY = mousePosition.Y - (mousePosition.Y * zoomFactor); - - // 更新 TranslateTransform,确保以鼠标位置为中心进行缩放 - translateTransform.X -= (mousePosition.X * (newScale - oldScale)); - translateTransform.Y -= (mousePosition.Y * (newScale - oldScale)); - } - } - - // 设置画布宽度高度 - private void InitializeCanvas(double width, double height) - { - FlowChartCanvas.Width = width; - FlowChartCanvas.Height = height; - } - - - #region 动态调整区域大小 - //private void Thumb_DragDelta_TopLeft(object sender, DragDeltaEventArgs e) - //{ - // // 从左上角调整大小 - // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); - // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); - - // FlowChartCanvas.Width = newWidth; - // FlowChartCanvas.Height = newHeight; - - // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); - // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); - //} - - //private void Thumb_DragDelta_TopRight(object sender, DragDeltaEventArgs e) - //{ - // // 从右上角调整大小 - // double newWidth = Math.Max(FlowChartCanvas.ActualWidth + e.HorizontalChange, 0); - // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); - - // FlowChartCanvas.Width = newWidth; - // FlowChartCanvas.Height = newHeight; - - // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); - //} - - //private void Thumb_DragDelta_BottomLeft(object sender, DragDeltaEventArgs e) - //{ - // // 从左下角调整大小 - // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); - // double newHeight = Math.Max(FlowChartCanvas.ActualHeight + e.VerticalChange, 0); - - // FlowChartCanvas.Width = newWidth; - // FlowChartCanvas.Height = newHeight; - - // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); - //} - - private void Thumb_DragDelta_BottomRight(object sender, DragDeltaEventArgs e) - { - // 获取缩放后的水平和垂直变化 - double horizontalChange = e.HorizontalChange * scaleTransform.ScaleX; - double verticalChange = e.VerticalChange * scaleTransform.ScaleY; - - // 计算新的宽度和高度,确保不会小于400 - double newWidth = Math.Max(FlowChartCanvas.ActualWidth + horizontalChange, 400); - double newHeight = Math.Max(FlowChartCanvas.ActualHeight + verticalChange, 400); - - newHeight = newHeight < 400 ? 400 : newHeight; - newWidth = newWidth < 400 ? 400 : newWidth; - - InitializeCanvas(newWidth, newHeight); - - //// 从右下角调整大小 - //double newWidth = Math.Max(FlowChartCanvas.ActualWidth + e.HorizontalChange * scaleTransform.ScaleX, 0); - //double newHeight = Math.Max(FlowChartCanvas.ActualHeight + e.VerticalChange * scaleTransform.ScaleY, 0); - - //newWidth = newWidth < 400 ? 400 : newWidth; - //newHeight = newHeight < 400 ? 400 : newHeight; - - //if (newWidth > 400 && newHeight > 400) - //{ - // FlowChartCanvas.Width = newWidth; - // FlowChartCanvas.Height = newHeight; - - // double x = e.HorizontalChange > 0 ? -0.5 : 0.5; - // double y = e.VerticalChange > 0 ? -0.5 : 0.5; - - // double deltaX = x * scaleTransform.ScaleX; - // double deltaY = y * scaleTransform.ScaleY; - // Test(deltaX, deltaY); - //} - } - - //private void Thumb_DragDelta_Left(object sender, DragDeltaEventArgs e) - //{ - // // 从左侧调整大小 - // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); - - // FlowChartCanvas.Width = newWidth; - // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); - //} - - private void Thumb_DragDelta_Right(object sender, DragDeltaEventArgs e) - { - //从右侧调整大小 - // 获取缩放后的水平变化 - double horizontalChange = e.HorizontalChange * scaleTransform.ScaleX; - - // 计算新的宽度,确保不会小于400 - double newWidth = Math.Max(FlowChartCanvas.ActualWidth + horizontalChange, 400); - - newWidth = newWidth < 400 ? 400 : newWidth; - InitializeCanvas(newWidth, FlowChartCanvas.Height); - - } - - //private void Thumb_DragDelta_Top(object sender, DragDeltaEventArgs e) - //{ - // // 从顶部调整大小 - // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); - - // FlowChartCanvas.Height = newHeight; - // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); - //} - - private void Thumb_DragDelta_Bottom(object sender, DragDeltaEventArgs e) - { - // 获取缩放后的垂直变化 - double verticalChange = e.VerticalChange * scaleTransform.ScaleY; - // 计算新的高度,确保不会小于400 - double newHeight = Math.Max(FlowChartCanvas.ActualHeight + verticalChange, 400); - newHeight = newHeight < 400 ? 400 : newHeight; - InitializeCanvas(FlowChartCanvas.Width, newHeight); - } - - - private void Test(double deltaX, double deltaY) - { - //Console.WriteLine((translateTransform.X, translateTransform.Y)); - //translateTransform.X += deltaX; - //translateTransform.Y += deltaY; - } - - #endregion - #endregion - - - - #endregion - - #region 画布中框选节点控件动作 - - /// - /// 在画布中尝试选取控件 - /// - /// - /// - private void FlowChartCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - if (GlobalJunctionData.MyGlobalConnectingData.IsCreateing) - { - return; - } - if (!IsSelectControl) - { - // 进入选取状态 - IsSelectControl = true; - IsSelectDragging = false; // 初始化为非拖动状态 - - // 记录鼠标起始点 - startSelectControolPoint = e.GetPosition(FlowChartCanvas); - - // 初始化选取矩形的位置和大小 - Canvas.SetLeft(SelectionRectangle, startSelectControolPoint.X); - Canvas.SetTop(SelectionRectangle, startSelectControolPoint.Y); - SelectionRectangle.Width = 0; - SelectionRectangle.Height = 0; - - // 显示选取矩形 - SelectionRectangle.Visibility = Visibility.Visible; - SelectionRectangle.ContextMenu ??= ConfiguerSelectionRectangle(); - - // 捕获鼠标,以便在鼠标移动到Canvas外部时仍能处理事件 - FlowChartCanvas.CaptureMouse(); - } - else - { - // 如果已经是选取状态,单击则认为结束框选 - CompleteSelection(); - } - - e.Handled = true; // 防止事件传播影响其他控件 - } - - /// - /// 在画布中释放鼠标按下,结束选取状态 / 停止创建连线,尝试连接节点 - /// - /// - /// - private void FlowChartCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) - { - if (IsSelectControl) - { - // 松开鼠标时判断是否为拖动操作 - if (IsSelectDragging) - { - // 完成拖动框选 - CompleteSelection(); - } - - // 释放鼠标捕获 - FlowChartCanvas.ReleaseMouseCapture(); - } - - // 创建连线 - if (GlobalJunctionData.MyGlobalConnectingData is ConnectingData myData && myData.IsCreateing) - { - - if (myData.IsCanConnected) - { - var canvas = this.FlowChartCanvas; - var currentendPoint = e.GetPosition(canvas); // 当前鼠标落点 - var changingJunctionPosition = myData.CurrentJunction.TranslatePoint(new Point(0, 0), canvas); - var changingJunctionRect = new Rect(changingJunctionPosition, new Size(myData.CurrentJunction.Width, myData.CurrentJunction.Height)); - - if (changingJunctionRect.Contains(currentendPoint)) // 可以创建连接 - { - #region 方法调用关系创建 - if (myData.Type == JunctionOfConnectionType.Invoke) - { - this.EnvDecorator.ConnectInvokeNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, - myData.StartJunction.JunctionType, - myData.CurrentJunction.JunctionType, - myData.ConnectionInvokeType); - } - #endregion - - #region 参数来源关系创建 - else if (myData.Type == JunctionOfConnectionType.Arg) - { - var argIndex = 0; - if (myData.StartJunction is ArgJunctionControl argJunction1) - { - argIndex = argJunction1.ArgIndex; - } - else if (myData.CurrentJunction is ArgJunctionControl argJunction2) - { - argIndex = argJunction2.ArgIndex; - } - - this.EnvDecorator.ConnectArgSourceNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, - myData.StartJunction.JunctionType, - myData.CurrentJunction.JunctionType, - myData.ConnectionArgSourceType, - argIndex); - } - #endregion - } - EndConnection(); - } - - } - e.Handled = true; - - } - - /// 完成选取操作 - /// - private void CompleteSelection() - { - IsSelectControl = false; - - // 隐藏选取矩形 - SelectionRectangle.Visibility = Visibility.Collapsed; - - // 获取选取范围 - Rect selectionArea = new Rect(Canvas.GetLeft(SelectionRectangle), - Canvas.GetTop(SelectionRectangle), - SelectionRectangle.Width, - SelectionRectangle.Height); - - // 处理选取范围内的控件 - // selectNodeControls.Clear(); - foreach (UIElement element in FlowChartCanvas.Children) - { - Rect elementBounds = new Rect(Canvas.GetLeft(element), Canvas.GetTop(element), - element.RenderSize.Width, element.RenderSize.Height); - - if (selectionArea.Contains(elementBounds)) - { - if (element is NodeControlBase control) - { - if (!selectNodeControls.Contains(control)) - { - selectNodeControls.Add(control); - } - } - } - } - - // 选中后的操作 - SelectedNode(); - } - private ContextMenu ConfiguerSelectionRectangle() - { - var contextMenu = new ContextMenu(); - contextMenu.Items.Add(CreateMenuItem("删除", (s, e) => - { - if (selectNodeControls.Count > 0) - { - foreach (var node in selectNodeControls.ToArray()) - { - var guid = node?.ViewModel?.NodeModel?.Guid; - if (!string.IsNullOrEmpty(guid)) - { - EnvDecorator.RemoveNodeAsync(guid); - } - } - } - SelectionRectangle.Visibility = Visibility.Collapsed; - })); - return contextMenu; - // nodeControl.ContextMenu = contextMenu; - } - private void SelectedNode() - { - - if (selectNodeControls.Count == 0) - { - //Console.WriteLine($"没有选择控件"); - SelectionRectangle.Visibility = Visibility.Collapsed; - return; - } - if(selectNodeControls.Count == 1) - { - // ChangeViewerObjOfNode(selectNodeControls[0]); - } - - //Console.WriteLine($"一共选取了{selectNodeControls.Count}个控件"); - foreach (var node in selectNodeControls) - { - //node.ViewModel.IsSelect =true; - // node.ViewModel.CancelSelect(); - node.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFC700")); - node.BorderThickness = new Thickness(4); - } - } - private void CancelSelectNode() - { - IsSelectControl = false; - foreach (var nodeControl in selectNodeControls) - { - //nodeControl.ViewModel.IsSelect = false; - nodeControl.BorderBrush = Brushes.Black; - nodeControl.BorderThickness = new Thickness(0); - if (nodeControl.ViewModel.NodeModel.IsStart) - { - nodeControl.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")); - nodeControl.BorderThickness = new Thickness(2); - } - } - selectNodeControls.Clear(); - } - #endregion - - #region 节点对齐 (有些小瑕疵) - - //public void UpdateConnectedLines() - //{ - // //foreach (var nodeControl in selectNodeControls) - // //{ - // // UpdateConnections(nodeControl); - // //} - // this.Dispatcher.Invoke(() => - // { - // foreach (var line in Connections) - // { - // line.AddOrRefreshLine(); // 节点完成对齐 - // } - // }); - - //} - - - #region Plan A 群组对齐 - - public void AlignControlsWithGrouping(List selectNodeControls, double proximityThreshold = 50, double spacing = 10) - { - if (selectNodeControls is null || selectNodeControls.Count < 2) - return; - - // 按照控件的相对位置进行分组 - var horizontalGroups = GroupByProximity(selectNodeControls, proximityThreshold, isHorizontal: true); - var verticalGroups = GroupByProximity(selectNodeControls, proximityThreshold, isHorizontal: false); - - // 对每个水平群组进行垂直对齐 - foreach (var group in horizontalGroups) - { - double avgY = group.Average(c => Canvas.GetTop(c)); // 计算Y坐标平均值 - foreach (var control in group) - { - Canvas.SetTop(control, avgY); // 对齐Y坐标 - } - } - - // 对每个垂直群组进行水平对齐 - foreach (var group in verticalGroups) - { - double avgX = group.Average(c => Canvas.GetLeft(c)); // 计算X坐标平均值 - foreach (var control in group) - { - Canvas.SetLeft(control, avgX); // 对齐X坐标 - } - } - } - - // 基于控件间的距离来分组,按水平或垂直方向 - private List> GroupByProximity(List controls, double proximityThreshold, bool isHorizontal) - { - var groups = new List>(); - - foreach (var control in controls) - { - bool addedToGroup = false; - - // 尝试将控件加入现有的群组 - foreach (var group in groups) - { - if (IsInProximity(group, control, proximityThreshold, isHorizontal)) - { - group.Add(control); - addedToGroup = true; - break; - } - } - - // 如果没有加入任何群组,创建新群组 - if (!addedToGroup) - { - groups.Add(new List { control }); - } - } - - return groups; - } - - // 判断控件是否接近某个群组 - private bool IsInProximity(List group, NodeControlBase control, double proximityThreshold, bool isHorizontal) - { - foreach (var existingControl in group) - { - double distance = isHorizontal - ? Math.Abs(Canvas.GetTop(existingControl) - Canvas.GetTop(control)) // 垂直方向的距离 - : Math.Abs(Canvas.GetLeft(existingControl) - Canvas.GetLeft(control)); // 水平方向的距离 - - if (distance <= proximityThreshold) - { - return true; - } - } - return false; - } - - #endregion - - #region Plan B 规划对齐 - public void AlignControlsWithDynamicProgramming(List selectNodeControls, double spacing = 10) - { - if (selectNodeControls is null || selectNodeControls.Count < 2) - return; - - int n = selectNodeControls.Count; - double[] dp = new double[n]; - int[] split = new int[n]; - - // 初始化动态规划数组 - for (int i = 1; i < n; i++) - { - dp[i] = double.MaxValue; - for (int j = 0; j < i; j++) - { - double cost = CalculateAlignmentCost(selectNodeControls, j, i, spacing); - if (dp[j] + cost < dp[i]) - { - dp[i] = dp[j] + cost; - split[i] = j; - } - } - } - - // 回溯找到最优的对齐方式 - AlignWithSplit(selectNodeControls, split, n - 1, spacing); - } - - // 计算从控件[j]到控件[i]的对齐代价,并考虑控件的大小和间距 - private double CalculateAlignmentCost(List controls, int start, int end, double spacing) - { - double totalWidth = 0; - double totalHeight = 0; - - for (int i = start; i <= end; i++) - { - totalWidth += controls[i].ActualWidth; - totalHeight += controls[i].ActualHeight; - } - - // 水平和垂直方向代价计算,包括控件大小和间距 - double widthCost = totalWidth + (end - start) * spacing; - double heightCost = totalHeight + (end - start) * spacing; - - // 返回较小的代价,表示更优的对齐方式 - return Math.Min(widthCost, heightCost); - } - - // 根据split数组调整控件位置,确保控件不重叠 - private void AlignWithSplit(List controls, int[] split, int end, double spacing) - { - if (end <= 0) - return; - - AlignWithSplit(controls, split, split[end], spacing); - - // 从split[end]到end的控件进行对齐操作 - double currentX = Canvas.GetLeft(controls[split[end]]); - double currentY = Canvas.GetTop(controls[split[end]]); - - for (int i = split[end] + 1; i <= end; i++) - { - // 水平或垂直对齐,确保控件之间有间距 - if (currentX + controls[i].ActualWidth + spacing <= Canvas.GetLeft(controls[end])) - { - Canvas.SetLeft(controls[i], currentX + controls[i].ActualWidth + spacing); - currentX += controls[i].ActualWidth + spacing; - } - else - { - Canvas.SetTop(controls[i], currentY + controls[i].ActualHeight + spacing); - currentY += controls[i].ActualHeight + spacing; - } - } - } - - #endregion - - public enum AlignMode - { - /// - /// 水平对齐 - /// - Horizontal, - /// - /// 垂直对齐 - /// - Vertical, - /// - /// 水平中心对齐 - /// - HorizontalCenter, - /// - /// 垂直中心对齐 - /// - VerticalCenter, - - /// - /// 规划对齐 - /// - Planning, - /// - /// 群组对齐 - /// - Grouping, - } - - - public void AlignControlsWithGrouping(List selectNodeControls, AlignMode alignMode, double proximityThreshold = 50, double spacing = 10) - { - if (selectNodeControls is null || selectNodeControls.Count < 2) - return; - - switch (alignMode) - { - case AlignMode.Horizontal: - AlignHorizontally(selectNodeControls, spacing);// AlignToCenter - break; - - case AlignMode.Vertical: - - AlignVertically(selectNodeControls, spacing); - break; - - case AlignMode.HorizontalCenter: - AlignToCenter(selectNodeControls, isHorizontal: false, spacing); - break; - - case AlignMode.VerticalCenter: - AlignToCenter(selectNodeControls, isHorizontal: true, spacing); - break; - - case AlignMode.Planning: - AlignControlsWithDynamicProgramming(selectNodeControls, spacing); - break; - case AlignMode.Grouping: - AlignControlsWithGrouping(selectNodeControls, proximityThreshold, spacing); - break; - } - - - } - - // 垂直对齐并避免重叠 - private void AlignHorizontally(List controls, double spacing) - { - double avgY = controls.Average(c => Canvas.GetTop(c)); // 计算Y坐标平均值 - double currentY = avgY; - - foreach (var control in controls.OrderBy(c => Canvas.GetTop(c))) // 按Y坐标排序对齐 - { - Canvas.SetTop(control, currentY); - currentY += control.ActualHeight + spacing; // 保证控件之间有足够的垂直间距 - } - } - - // 水平对齐并避免重叠 - private void AlignVertically(List controls, double spacing) - { - double avgX = controls.Average(c => Canvas.GetLeft(c)); // 计算X坐标平均值 - double currentX = avgX; - - foreach (var control in controls.OrderBy(c => Canvas.GetLeft(c))) // 按X坐标排序对齐 - { - Canvas.SetLeft(control, currentX); - currentX += control.ActualWidth + spacing; // 保证控件之间有足够的水平间距 - } - } - - // 按中心点对齐 - private void AlignToCenter(List controls, bool isHorizontal, double spacing) - { - double avgCenter = isHorizontal - ? controls.Average(c => Canvas.GetLeft(c) + c.ActualWidth / 2) // 水平中心点 - : controls.Average(c => Canvas.GetTop(c) + c.ActualHeight / 2); // 垂直中心点 - - foreach (var control in controls) - { - if (isHorizontal) - { - double left = avgCenter - control.ActualWidth / 2; - Canvas.SetLeft(control, left); - } - else - { - double top = avgCenter - control.ActualHeight / 2; - Canvas.SetTop(control, top); - } - } - } - - #endregion - - #region 静态方法:创建节点,创建菜单子项,获取区域 - - /// - /// 创建节点控件 - /// - /// 节点控件视图控件类型 - /// 节点控件ViewModel类型 - /// 节点Model实例 - /// 节点所在画布 - /// - /// 无法创建节点控件 - private static NodeControlBase CreateNodeControl(Type controlType, Type viewModelType, NodeModelBase model, Canvas nodeCanvas) - { - if ((controlType is null) - || viewModelType is null - || model is null) - { - throw new Exception("无法创建节点控件"); - } - if (typeof(NodeControlBase).IsSubclassOf(controlType) || typeof(NodeControlViewModelBase).IsSubclassOf(viewModelType)) - { - throw new Exception("无法创建节点控件"); - } - - if (string.IsNullOrEmpty(model.Guid)) - { - model.Guid = Guid.NewGuid().ToString(); - } - - var viewModel = Activator.CreateInstance(viewModelType, [model]); - var controlObj = Activator.CreateInstance(controlType, [viewModel]); - if (controlObj is NodeControlBase nodeControl) - { - nodeControl.NodeCanvas = nodeCanvas; - return nodeControl; - } - else - { - throw new Exception("无法创建节点控件"); - } - } - - - /// - /// 创建菜单子项 - /// - /// - /// - /// - public static MenuItem CreateMenuItem(string header, RoutedEventHandler handler) - { - var menuItem = new MenuItem { Header = header }; - menuItem.Click += handler; - return menuItem; - } - - - - /// - /// 穿透元素获取区域容器 - /// - /// - /// - /// - public static T? GetParentOfType(DependencyObject element) where T : DependencyObject - { - while (element != null) - { - if (element is T e) - { - return e; - } - element = VisualTreeHelper.GetParent(element); - } - return null; - } - - #endregion - - #region 节点树、IOC视图管理 - - private void JudgmentFlipFlopNode(NodeControlBase nodeControl) - { - if (nodeControl is FlipflopNodeControl flipflopControl - && flipflopControl?.ViewModel?.NodeModel is NodeModelBase nodeModel) // 判断是否为触发器 - { - int count = 0; - foreach (var ct in NodeStaticConfig.ConnectionTypes) - { - count += nodeModel.PreviousNodes[ct].Count; - } - if (count == 0) - { - NodeTreeViewer.AddGlobalFlipFlop(EnvDecorator, nodeModel); // 添加到全局触发器树树视图 - } - else - { - NodeTreeViewer.RemoveGlobalFlipFlop(nodeModel); // 从全局触发器树树视图中移除 - } - } - } - void LoadIOCObjectViewer() - { - - } - #endregion - - - - #region 顶部菜单栏 - 调试功能区 - - /// - /// 运行测试 - /// - /// - /// - private async void ButtonDebugRun_Click(object sender, RoutedEventArgs e) - { - LogOutWindow?.Show(); - - - -#if WINDOWS - //Dispatcher uiDispatcher = Application.Current.MainWindow.Dispatcher; - //SynchronizationContext? uiContext = SynchronizationContext.Current; - //EnvDecorator.IOC.CustomRegisterInstance(typeof(SynchronizationContextk).FullName, uiContext, false); -#endif - - // 获取主线程的 SynchronizationContext - Action uiInvoke = (uiContext, action) => uiContext?.Post(state => action?.Invoke(), null); - - SereinEnv.WriteLine(InfoType.INFO, "流程开始运行"); - try - { - await EnvDecorator.StartFlowAsync(); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - - // await EnvDecorator.StartAsync(); - //await Task.Factory.StartNew(FlowEnvironment.StartAsync); - } - - /// - /// 退出 - /// - /// - /// - private async void ButtonDebugFlipflopNode_Click(object sender, RoutedEventArgs e) - { - try - { - await EnvDecorator.ExitFlowAsync(); // 在运行平台上点击了退出 - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - - /// - /// 从选定的节点开始运行 - /// - /// - /// - private async void ButtonStartFlowInSelectNode_Click(object sender, RoutedEventArgs e) - { - if (selectNodeControls.Count == 0) - { - SereinEnv.WriteLine(InfoType.INFO, "请至少选择一个节点"); - } - else if (selectNodeControls.Count > 1) - { - SereinEnv.WriteLine(InfoType.INFO, "请只选择一个节点"); - } - try - { - await this.EnvDecorator.StartAsyncInSelectNode(selectNodeControls[0].ViewModel.NodeModel.Guid); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - - - - - #endregion - - #region 顶部菜单栏 - 项目文件菜单 - - - /// - /// 保存为项目文件 - /// - /// - /// - private async void ButtonSaveFile_Click(object sender, RoutedEventArgs e) - { - try - { - EnvDecorator.SaveProject(); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - - - /// - /// 打开本地项目文件 - /// - /// - /// - private void ButtonOpenLocalProject_Click(object sender, RoutedEventArgs e) - { - - } - - - -#endregion - - #region 顶部菜单栏 - 视图管理 - /// - /// 重置画布 - /// - /// - /// - private void ButtonResetCanvas_Click(object sender, RoutedEventArgs e) - { - translateTransform.X = 0; - translateTransform.Y = 0; - scaleTransform.ScaleX = 1; - scaleTransform.ScaleY = 1; - } - /// - /// 查看输出日志窗口 - /// - /// - /// - private void ButtonOpenConsoleOutWindow_Click(object sender, RoutedEventArgs e) - { - LogOutWindow?.Show(); - } - /// - /// 定位节点 - /// - /// - /// - private void ButtonLocationNode_Click(object sender, RoutedEventArgs e) - { - InputDialog inputDialog = new InputDialog(); - inputDialog.Closed += (s, e) => - { - var nodeGuid = inputDialog.InputValue; - EnvDecorator.NodeLocated(nodeGuid); - }; - inputDialog.ShowDialog(); - } - - #endregion - - #region 顶部菜单栏 - 远程管理 - private async void ButtonStartRemoteServer_Click(object sender, RoutedEventArgs e) - { - try - { - await this.EnvDecorator.StartRemoteServerAsync(); - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - } - - /// - /// 连接远程运行环境 - /// - /// - /// - private void ButtonConnectionRemoteEnv_Click(object sender, RoutedEventArgs e) - { - var windowEnvRemoteLoginView = new WindowEnvRemoteLoginView(async (addres, port, token) => - { - ResetFlowEnvironmentEvent();// 移除事件 - (var isConnect, var _) = await this.EnvDecorator.ConnectRemoteEnv(addres, port, token); - InitFlowEnvironmentEvent(); // 重新添加事件(如果没有连接成功,那么依然是原本的环境) - if (isConnect) - { - // 连接成功,加载远程项目 - _ = Task.Run(async () => - { - try - { - var flowEnvInfo = await EnvDecorator.GetEnvInfoAsync(); - EnvDecorator.LoadProject(flowEnvInfo, string.Empty);// 加载远程环境的项目 - } - catch (Exception ex) - { - SereinEnv.WriteLine(ex); - return; - } - }); - - } - }); - windowEnvRemoteLoginView.Show(); - - } - #endregion - - - - /// - /// 窗体按键监听。 - /// - /// - /// - private void Window_PreviewKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Tab) - { - e.Handled = true; // 禁止默认的Tab键行为 - } - - #region 复制粘贴选择的节点 - if (Keyboard.Modifiers == ModifierKeys.Control) - { - if (e.Key == Key.C && selectNodeControls.Count > 0) - { - CpoyNodeInfo(); - } - else if (e.Key == Key.V) - { - PasteNodeInfo(); - } - } - #endregion - if (e.KeyStates == Keyboard.GetKeyStates(Key.Escape)) - { - IsControlDragging = false; - IsCanvasDragging = false; - SelectionRectangle.Visibility = Visibility.Collapsed; - CancelSelectNode(); - EndConnection(); - } - - if(GlobalJunctionData.MyGlobalConnectingData is ConnectingData myData && myData.IsCreateing) - { - if(myData.Type == JunctionOfConnectionType.Invoke) - { - ConnectionInvokeType connectionInvokeType = e.KeyStates switch - { - KeyStates k when k == Keyboard.GetKeyStates(Key.D1) => ConnectionInvokeType.Upstream, - KeyStates k when k == Keyboard.GetKeyStates(Key.D2) => ConnectionInvokeType.IsSucceed, - KeyStates k when k == Keyboard.GetKeyStates(Key.D3) => ConnectionInvokeType.IsFail, - KeyStates k when k == Keyboard.GetKeyStates(Key.D4) => ConnectionInvokeType.IsError, - _ => ConnectionInvokeType.None, - }; - - if (connectionInvokeType != ConnectionInvokeType.None) - { - myData.ConnectionInvokeType = connectionInvokeType; - myData.MyLine.Line.UpdateLineColor(connectionInvokeType.ToLineColor()); - } - } - else if (myData.Type == JunctionOfConnectionType.Arg) - { - ConnectionArgSourceType connectionArgSourceType = e.KeyStates switch - { - KeyStates k when k == Keyboard.GetKeyStates(Key.D1) => ConnectionArgSourceType.GetOtherNodeData, - KeyStates k when k == Keyboard.GetKeyStates(Key.D2) => ConnectionArgSourceType.GetOtherNodeDataOfInvoke, - _ => ConnectionArgSourceType.GetPreviousNodeData, - }; - - if (connectionArgSourceType != ConnectionArgSourceType.GetPreviousNodeData) - { - myData.ConnectionArgSourceType = connectionArgSourceType; - myData.MyLine.Line.UpdateLineColor(connectionArgSourceType.ToLineColor()); - } - } - myData.CurrentJunction.InvalidateVisual(); // 刷新目标节点控制点样式 - - } - - - } - - #region 复制节点,粘贴节点 - - /// - /// 复制节点 - /// - private void CpoyNodeInfo() - { - if(selectNodeControls.Count == 0) - { - return; - } - // 处理复制操作 - var dictSelection = selectNodeControls - .Select(control => control.ViewModel.NodeModel).ToList(); - - - // 遍历当前已选节点 - foreach (var node in dictSelection.ToArray()) - { - if(node.ChildrenNode.Count == 0) - { - continue; - } - // 遍历这些节点的子节点,添加过来 - foreach (var childNode in node.ChildrenNode) - { - dictSelection.Add(childNode); - } - } - - var nodeInfos = dictSelection.Select(item => item.ToInfo()); - - JObject json = new JObject() - { - ["nodes"] = JArray.FromObject(nodeInfos) - }; - - var jsonText = json.ToString(); - - - try - { - //Clipboard.SetDataObject(result, true); // 持久性设置 - Clipboard.SetDataObject(jsonText, true); // 持久性设置 - SereinEnv.WriteLine(InfoType.INFO, $"复制已选节点({dictSelection.Count}个)"); - } - catch (Exception ex) - { - SereinEnv.WriteLine(InfoType.ERROR, $"复制失败:{ex.Message}"); - } - } - - /// - /// 粘贴节点 - /// - private void PasteNodeInfo() - { - if (Clipboard.ContainsText()) - { - try - { - - string clipboardText = Clipboard.GetText(TextDataFormat.Text); - string jsonText = JObject.Parse(clipboardText)["nodes"].ToString(); - List nodes = JsonConvert.DeserializeObject>(jsonText); - if (nodes is null || nodes.Count < 0) - { - return; - } - - #region 节点去重 - Dictionary guids = new Dictionary(); // 记录 Guid - // 遍历当前已选节点 - foreach (var node in nodes.ToArray()) - { - if (NodeControls.ContainsKey(node.Guid) && !guids.ContainsKey(node.Guid)) - { - // 如果是没出现过、且在当前记录中重复的Guid,则记录并新增对应的映射。 - guids.TryAdd(node.Guid, Guid.NewGuid().ToString()); - } - else - { - // 出现过的Guid,说明重复添加了。应该不会走到这。 - continue; - } - - if (node.ChildNodeGuids is null) - { - continue; // 跳过没有子节点的节点 - } - - // 遍历这些节点的子节点,获得完整的已选节点信息 - foreach (var childNodeGuid in node.ChildNodeGuids) - { - if (NodeControls.ContainsKey(node.Guid) && !NodeControls.ContainsKey(node.Guid)) - { - // 当前Guid并不重复,跳过替换 - continue; - } - if (!guids.ContainsKey(childNodeGuid)) - { - // 如果是没出现过的Guid,则记录并新增对应的映射。 - guids.TryAdd(node.Guid, Guid.NewGuid().ToString()); - } - - if (!string.IsNullOrEmpty(childNodeGuid) - && NodeControls.TryGetValue(childNodeGuid, out var nodeControl)) - { - - var newNodeInfo = nodeControl.ViewModel.NodeModel.ToInfo(); - nodes.Add(newNodeInfo); - } - } - } - - //var flashText = new FlashText.NET.TextReplacer(); - - //var t = guids.Select(kvp => (kvp.Key, kvp.Value)).ToArray(); - //var result = flashText.ReplaceWords(jsonText, t); - - StringBuilder sb = new StringBuilder(jsonText); - foreach (var kv in guids) - { - sb.Replace(kv.Key, kv.Value); - } - string result = sb.ToString(); - - - /*var replacer = new GuidReplacer(); - foreach (var kv in guids) - { - replacer.AddReplacement(kv.Key, kv.Value); - } - string result = replacer.Replace(jsonText);*/ - - - //SereinEnv.WriteLine(InfoType.ERROR, result); - nodes = JsonConvert.DeserializeObject>(result); - - if (nodes is null || nodes.Count < 0) - { - return; - } - #endregion - - Point mousePosition = Mouse.GetPosition(FlowChartCanvas); - PositionOfUI positionOfUI = new PositionOfUI(mousePosition.X, mousePosition.Y); // 坐标数据 - - // 获取第一个节点的原始位置 - var index0NodeX = nodes[0].Position.X; - var index0NodeY = nodes[0].Position.Y; - - // 计算所有节点相对于第一个节点的偏移量 - foreach (var node in nodes) - { - - var offsetX = node.Position.X - index0NodeX; - var offsetY = node.Position.Y - index0NodeY; - - // 根据鼠标位置平移节点 - node.Position = new PositionOfUI(positionOfUI.X + offsetX, positionOfUI.Y + offsetY); - } - - _ = EnvDecorator.LoadNodeInfosAsync(nodes); - } - catch (Exception ex) - { - - //SereinEnv.WriteLine(InfoType.ERROR, $"粘贴节点时发生异常:{ex}"); - } - // SereinEnv.WriteLine(InfoType.INFO, $"剪贴板文本内容: {clipboardText}"); - } - else if (Clipboard.ContainsImage()) - { - // var image = Clipboard.GetImage(); - } - else - { - SereinEnv.WriteLine(InfoType.INFO, "剪贴板中没有可识别的数据。"); - } - } - - #endregion - - - - /* /// - /// 对象装箱测试 - /// - /// - /// - private void ButtonTestExpObj_Click(object sender, RoutedEventArgs e) - { - //string jsonString = - //""" - //{ - // "Name": "张三", - // "Age": 24, - // "Address": { - // "City": "北京", - // "PostalCode": "10000" - // } - //} - //"""; - - var externalData = new Dictionary - { - { "Name", "John" }, - { "Age", 30 }, - { "Addresses", new List> - { - new Dictionary - { - { "Street", "123 Main St" }, - { "City", "New York" } - }, - new Dictionary - { - { "Street", "456 Another St" }, - { "City", "Los Angeles" } - } - } - } - }; - - if (!ObjDynamicCreateHelper.TryResolve(externalData, "RootType",out var result)) - { - SereinEnv.WriteLine(InfoType.ERROR, "赋值过程中有错误,请检查属性名和类型!"); - return; - } - ObjDynamicCreateHelper.PrintObjectProperties(result!); - var exp = "@set .Addresses[1].Street = 233"; - var data = SerinExpressionEvaluator.Evaluate(exp, result!, out bool isChange); - exp = "@get .Addresses[1].Street"; - data = SerinExpressionEvaluator.Evaluate(exp,result!, out isChange); - SereinEnv.WriteLine(InfoType.INFO, $"{exp} => {data}"); - } -*/ - } -} \ No newline at end of file diff --git a/WorkBench/MainWindowViewModel.cs b/WorkBench/MainWindowViewModel.cs deleted file mode 100644 index 0aedffd..0000000 --- a/WorkBench/MainWindowViewModel.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Serein.Library.Api; -using Serein.Library.Utils; -using Serein.NodeFlow.Env; -using System.ComponentModel; -using System.Windows; - -namespace Serein.Workbench -{ - /// - /// 工作台数据视图 - /// - /// - public class MainWindowViewModel: INotifyPropertyChanged - { - private readonly MainWindow window ; - - /// - /// 运行环境 - /// - public IFlowEnvironment FlowEnvironment { get; set; } - - /// - /// 工作台数据视图 - /// - /// - public MainWindowViewModel(MainWindow window) - { - UIContextOperation? uIContextOperation = null; - Application.Current.Dispatcher.Invoke(() => - { - SynchronizationContext? uiContext = SynchronizationContext.Current; // 在UI线程上获取UI线程上下文信息 - if (uiContext != null) - { - uIContextOperation = new UIContextOperation(uiContext); // 封装一个调用UI线程的工具类 - } - }); - - if (uIContextOperation is null) - { - throw new Exception("无法封装 UIContextOperation "); - } - else - { - FlowEnvironment = new FlowEnvironmentDecorator(uIContextOperation); - //_ = FlowEnvironment.StartRemoteServerAsync(); - this.window = window; - } - } - - - private bool _isConnectionInvokeNode = false; - /// - /// 是否正在连接节点的方法调用关系 - /// - public bool IsConnectionInvokeNode { get => _isConnectionInvokeNode; set - { - if (_isConnectionInvokeNode != value) - { - SetProperty(ref _isConnectionInvokeNode, value); - } - } - } - - private bool _isConnectionArgSouceNode = false; - /// - /// 是否正在连接节点的参数传递关系 - /// - public bool IsConnectionArgSourceNode { get => _isConnectionArgSouceNode; set - { - if (_isConnectionArgSouceNode != value) - { - SetProperty(ref _isConnectionArgSouceNode, value); - } - } - } - - - /// - /// 略 - /// 此事件为自动生成 - /// - public event PropertyChangedEventHandler? PropertyChanged; - /// - /// 通知属性变更 - /// - /// 类型 - /// 绑定的变量 - /// 新的数据 - /// - protected void SetProperty(ref T storage, T value, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) - { - if (Equals(storage, value)) - { - return; - } - - storage = value; - PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/WorkBench/Node/NodeControlViewModelBase.cs b/WorkBench/Node/NodeControlViewModelBase.cs deleted file mode 100644 index ed58ff9..0000000 --- a/WorkBench/Node/NodeControlViewModelBase.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.ComponentModel; -using Serein.Library; -using System.Runtime.CompilerServices; -using System.Windows.Controls; -using System.Windows.Data; -using System; - -namespace Serein.Workbench.Node.ViewModel -{ - public abstract class NodeControlViewModelBase - { - ///// - ///// 对应的节点实体类 - ///// - public NodeModelBase NodeModel { get; } - - public NodeControlViewModelBase(NodeModelBase nodeModel) - { - NodeModel = nodeModel; - - } - - - private bool isInterrupt; - ///// - ///// 控制中断状态的视觉效果 - ///// - public bool IsInterrupt - { - get => NodeModel.DebugSetting.IsInterrupt; - set - { - NodeModel.DebugSetting.IsInterrupt = value; - OnPropertyChanged(); - } - } - - - - public event PropertyChangedEventHandler? PropertyChanged; - protected void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - - } -} diff --git a/WorkBench/Node/View/ActionNodeControl.xaml b/WorkBench/Node/View/ActionNodeControl.xaml deleted file mode 100644 index fb71a3f..0000000 --- a/WorkBench/Node/View/ActionNodeControl.xaml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WorkBench/Node/View/ActionNodeControl.xaml.cs b/WorkBench/Node/View/ActionNodeControl.xaml.cs deleted file mode 100644 index f26e7ba..0000000 --- a/WorkBench/Node/View/ActionNodeControl.xaml.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.ViewModel; -using System.Runtime.CompilerServices; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; - -namespace Serein.Workbench.Node.View -{ - /// - /// ActionNode.xaml 的交互逻辑 - /// - public partial class ActionNodeControl : NodeControlBase, INodeJunction - { - public ActionNodeControl(ActionNodeControlViewModel viewModel) : base(viewModel) - { - DataContext = viewModel; - InitializeComponent(); - if(ExecuteJunctionControl.MyNode != null) - { - - ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid; - } - } - - /// - /// 入参控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; - - /// - /// 下一个调用方法控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; - - /// - /// 返回值控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ReturnDataJunction => this.ResultJunctionControl; - - /// - /// 方法入参控制点(可能有,可能没) - /// - JunctionControlBase[] INodeJunction.ArgDataJunction - { - get - { - // 获取 MethodDetailsControl 实例 - var methodDetailsControl = this.MethodDetailsControl; - var itemsControl = FindVisualChild(methodDetailsControl); // 查找 ItemsControl - if (itemsControl != null) - { - var argDataJunction = new JunctionControlBase[base.ViewModel.NodeModel.MethodDetails.ParameterDetailss.Length]; - var controls = new List(); - - for (int i = 0; i < itemsControl.Items.Count; i++) - { - var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement; - if (container != null) - { - var argControl = FindVisualChild(container); - if (argControl != null) - { - controls.Add(argControl); // 收集 ArgJunctionControl 实例 - } - } - } - return argDataJunction = controls.ToArray(); - } - else - { - return []; - } - } - - - } - - - - - } -} diff --git a/WorkBench/Node/View/ConditionNodeControl.xaml b/WorkBench/Node/View/ConditionNodeControl.xaml deleted file mode 100644 index f6f8c07..0000000 --- a/WorkBench/Node/View/ConditionNodeControl.xaml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WorkBench/Node/View/ConditionNodeControl.xaml.cs b/WorkBench/Node/View/ConditionNodeControl.xaml.cs deleted file mode 100644 index f1ce898..0000000 --- a/WorkBench/Node/View/ConditionNodeControl.xaml.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.ViewModel; - -namespace Serein.Workbench.Node.View -{ - /// - /// ConditionNode.xaml 的交互逻辑 - /// - public partial class ConditionNodeControl : NodeControlBase, INodeJunction - { - public ConditionNodeControl() : base() - { - // 窗体初始化需要 - base.ViewModel = new ConditionNodeControlViewModel (new SingleConditionNode(null)); - DataContext = ViewModel; - InitializeComponent(); - } - - public ConditionNodeControl(ConditionNodeControlViewModel viewModel):base(viewModel) - { - DataContext = viewModel; - InitializeComponent(); - } - - /// - /// 入参控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; - - /// - /// 下一个调用方法控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; - - /// - /// 返回值控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ReturnDataJunction => this.ResultJunctionControl; - - /// - /// 方法入参控制点(可能有,可能没) - /// - private JunctionControlBase[] argDataJunction; - /// - /// 方法入参控制点(可能有,可能没) - /// - JunctionControlBase[] INodeJunction.ArgDataJunction - { - get - { - argDataJunction = new JunctionControlBase[1]; - argDataJunction[0] = this.ArgJunctionControl; - return argDataJunction; - } - } - - } -} diff --git a/WorkBench/Node/View/ConditionRegionControl.xaml b/WorkBench/Node/View/ConditionRegionControl.xaml deleted file mode 100644 index c7932cc..0000000 --- a/WorkBench/Node/View/ConditionRegionControl.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - diff --git a/WorkBench/Themes/WindowDialogInput.xaml.cs b/WorkBench/Themes/WindowDialogInput.xaml.cs deleted file mode 100644 index f5f5441..0000000 --- a/WorkBench/Themes/WindowDialogInput.xaml.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Serein.Library; -using Serein.Library.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; - -namespace Serein.Workbench.Themes -{ - /// - /// WindowDialogInput.xaml 的交互逻辑 - /// - public partial class WindowEnvRemoteLoginView : Window - { - private Action ConnectRemoteFlowEnv; - - /// - /// 弹窗输入 - /// - /// - public WindowEnvRemoteLoginView(Action connectRemoteFlowEnv) - { - WindowStartupLocation = WindowStartupLocation.CenterScreen; - InitializeComponent(); - ConnectRemoteFlowEnv = connectRemoteFlowEnv; - } - - private void ButtonTestConnect_Client(object sender, RoutedEventArgs e) - { - var addres = this.TextBlockAddres.Text; - _ = int.TryParse(this.TextBlockPort.Text, out var port); - _ = Task.Run(() => { - bool success = false; - try - { - TcpClient tcpClient = new TcpClient(); - var result = tcpClient.BeginConnect(addres, port, null, null); - success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3)); - } - catch - { - success = false; - } - if (!success) - { - SereinEnv.WriteLine(InfoType.ERROR, $"无法连接远程:{addres}:{port}"); - } - }); - - } - - private void ButtonTestLoginEnv_Client(object sender, RoutedEventArgs e) - { - var addres = this.TextBlockAddres.Text; - _ = int.TryParse(this.TextBlockPort.Text, out var port); - var token = this.TextBlockToken.Text; - ConnectRemoteFlowEnv?.Invoke(addres, port, token); - } - } -} diff --git a/WorkBench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs b/WorkBench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs deleted file mode 100644 index e830c6d..0000000 --- a/WorkBench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Data; -using System.Windows; - -namespace Serein.Workbench.Tool.Converters -{ - /// - /// 根据bool类型控制可见性 - /// - [ValueConversion(typeof(bool), typeof(Visibility))] - public class InvertableBooleanToVisibilityConverter : IValueConverter - { - enum Parameters - { - Normal, Inverted - } - - public object Convert(object value, Type targetType, - object parameter, CultureInfo culture) - { - var boolValue = (bool)value; - var direction = (Parameters)Enum.Parse(typeof(Parameters), (string)parameter); - - if (direction == Parameters.Inverted) - return !boolValue ? Visibility.Visible : Visibility.Collapsed; - - return boolValue ? Visibility.Visible : Visibility.Collapsed; - } - - public object? ConvertBack(object value, Type targetType, - object parameter, CultureInfo culture) - { - return null; - } - } -} diff --git a/WorkBench/Tool/Converters/ThumbPositionConverter.cs b/WorkBench/Tool/Converters/ThumbPositionConverter.cs deleted file mode 100644 index df6adbf..0000000 --- a/WorkBench/Tool/Converters/ThumbPositionConverter.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Data; - -namespace Serein.Workbench.Tool.Converters -{ - /// - /// 画布拉动范围距离计算器 - /// - public class RightThumbPositionConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - if (value is double width) - return width - 10; // Adjust for Thumb width - return 0; - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - throw new NotImplementedException(); - } - } - /// - /// 画布拉动范围距离计算器 - /// - public class BottomThumbPositionConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - if (value is double height) - return height - 10; // Adjust for Thumb height - return 0; - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - throw new NotImplementedException(); - } - } - /// - /// 画布拉动范围距离计算器 - /// - public class VerticalCenterThumbPositionConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - if (value is double height) - return height / 2 - 5; // Centering Thumb vertically - return 0; - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - throw new NotImplementedException(); - } - } - /// - /// 画布拉动范围距离计算器 - /// - public class HorizontalCenterThumbPositionConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - if (value is double width) - return width / 2 - 5; // Centering Thumb horizontally - return 0; - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) - { - throw new NotImplementedException(); - } - } - -} diff --git a/WorkBench/Tool/Converters/TypeToColorConverter.cs b/WorkBench/Tool/Converters/TypeToColorConverter.cs deleted file mode 100644 index 6d27425..0000000 --- a/WorkBench/Tool/Converters/TypeToColorConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Serein.Library; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; - -namespace Serein.Workbench.Tool.Converters -{ - /// - /// 根据控件类型切换颜色 - /// - public class TypeToColorConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - // 根据 ControlType 返回颜色 - return value switch - { - NodeControlType.Action => Brushes.Blue, - NodeControlType.Flipflop => Brushes.Green, - _ => Brushes.Black, - }; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); - } -} From 2c160d660ad40d165aa20a3a39dd64fe24398137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=80=E6=B3=93=E7=A7=8B=E6=B0=B4?= <1090698674@qq.com> Date: Thu, 2 Jan 2025 13:58:55 +0800 Subject: [PATCH 3/4] Delete Workbench directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除不要的文件夹 --- Workbench/Extension/LineExtension.cs | 53 --- Workbench/Extension/MyExtension.cs | 42 -- Workbench/Node/INodeContainerControl.cs | 33 -- Workbench/Node/INodeJunction.cs | 52 --- .../Node/Junction/ConnectionLineShape.cs | 234 ----------- .../Node/Junction/JunctionControlBase.cs | 378 ------------------ Workbench/Node/Junction/JunctionData.cs | 161 -------- .../Node/Junction/NodeJunctionViewBase.cs | 237 ----------- .../Node/Junction/View/ArgJunctionControl.cs | 74 ---- .../Junction/View/ExecuteJunctionControl.cs | 84 ---- .../Junction/View/NextStepJunctionControl.cs | 60 --- .../Junction/View/ResultJunctionControl.cs | 61 --- Workbench/Node/NodeControlBase.cs | 198 --------- Workbench/Node/RelayCommand.cs | 26 -- Workbench/Node/View/ConnectionControl.cs | 297 -------------- Workbench/Node/View/GlobalDataControl.xaml | 87 ---- Workbench/Node/View/GlobalDataControl.xaml.cs | 87 ---- Workbench/Node/View/ScriptNodeControl.xaml | 93 ----- Workbench/Node/View/ScriptNodeControl.xaml.cs | 162 -------- .../ViewModel/ExpOpNodeControlViewModel.cs | 26 -- .../GlobalDataNodeControlViewModel.cs | 51 --- .../ViewModel/ScriptNodeControlViewModel.cs | 62 --- Workbench/Properties/launchSettings.json | 7 - .../Serein.Workbench_wjzi1sgn_wpftmp.csproj | 292 -------------- Workbench/Themes/BindableRichTextBox.cs | 28 -- Workbench/Tool/GuidReplacer.cs | 68 ---- 26 files changed, 2953 deletions(-) delete mode 100644 Workbench/Extension/LineExtension.cs delete mode 100644 Workbench/Extension/MyExtension.cs delete mode 100644 Workbench/Node/INodeContainerControl.cs delete mode 100644 Workbench/Node/INodeJunction.cs delete mode 100644 Workbench/Node/Junction/ConnectionLineShape.cs delete mode 100644 Workbench/Node/Junction/JunctionControlBase.cs delete mode 100644 Workbench/Node/Junction/JunctionData.cs delete mode 100644 Workbench/Node/Junction/NodeJunctionViewBase.cs delete mode 100644 Workbench/Node/Junction/View/ArgJunctionControl.cs delete mode 100644 Workbench/Node/Junction/View/ExecuteJunctionControl.cs delete mode 100644 Workbench/Node/Junction/View/NextStepJunctionControl.cs delete mode 100644 Workbench/Node/Junction/View/ResultJunctionControl.cs delete mode 100644 Workbench/Node/NodeControlBase.cs delete mode 100644 Workbench/Node/RelayCommand.cs delete mode 100644 Workbench/Node/View/ConnectionControl.cs delete mode 100644 Workbench/Node/View/GlobalDataControl.xaml delete mode 100644 Workbench/Node/View/GlobalDataControl.xaml.cs delete mode 100644 Workbench/Node/View/ScriptNodeControl.xaml delete mode 100644 Workbench/Node/View/ScriptNodeControl.xaml.cs delete mode 100644 Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs delete mode 100644 Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs delete mode 100644 Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs delete mode 100644 Workbench/Properties/launchSettings.json delete mode 100644 Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj delete mode 100644 Workbench/Themes/BindableRichTextBox.cs delete mode 100644 Workbench/Tool/GuidReplacer.cs diff --git a/Workbench/Extension/LineExtension.cs b/Workbench/Extension/LineExtension.cs deleted file mode 100644 index ce0eb42..0000000 --- a/Workbench/Extension/LineExtension.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Serein.Library; -using Serein.Workbench.Node.View; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media; - -namespace Serein.Workbench.Extension -{ - /// - /// 线条颜色 - /// - public static class LineExtension - { - /// - /// 根据连接类型指定颜色 - /// - /// - /// - /// - public static SolidColorBrush ToLineColor(this ConnectionInvokeType currentConnectionType) - { - return currentConnectionType switch - { - ConnectionInvokeType.IsSucceed => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")), // 04FC10 & 027E08 - ConnectionInvokeType.IsFail => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F18905")), - ConnectionInvokeType.IsError => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FE1343")), - ConnectionInvokeType.Upstream => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A82E4")), - ConnectionInvokeType.None => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), - _ => throw new Exception(), - }; - } - /// - /// 根据连接类型指定颜色 - /// - /// - /// - /// - public static SolidColorBrush ToLineColor(this ConnectionArgSourceType connection) - { - return connection switch - { - ConnectionArgSourceType.GetPreviousNodeData => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), // 04FC10 & 027E08 - ConnectionArgSourceType.GetOtherNodeData => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), - ConnectionArgSourceType.GetOtherNodeDataOfInvoke => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B06BBB")), - _ => throw new Exception(), - }; - } - - } -} diff --git a/Workbench/Extension/MyExtension.cs b/Workbench/Extension/MyExtension.cs deleted file mode 100644 index 0bacb12..0000000 --- a/Workbench/Extension/MyExtension.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; - -namespace Serein.Workbench.Extension -{ - public static class PointExtension - { - public static Point Add(this Point a, Point b) - { - return new Point(a.X + b.X, a.Y + b.Y); - } - - public static Point Sub(this Point a, Point b) - { - return new Point(a.X - b.X, a.Y - b.Y); - } - - public static Vector ToVector(this Point me) - { - return new Vector(me.X, me.Y); - } - } - public static class VectorExtension - { - public static double DotProduct(this Vector a, Vector b) - { - return a.X * b.X + a.Y * b.Y; - } - - public static Vector NormalizeTo(this Vector v) - { - var temp = v; - temp.Normalize(); - - return temp; - } - } -} diff --git a/Workbench/Node/INodeContainerControl.cs b/Workbench/Node/INodeContainerControl.cs deleted file mode 100644 index 4c01eff..0000000 --- a/Workbench/Node/INodeContainerControl.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Serein.Workbench.Node.View; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Serein.Workbench.Node -{ - - /// - /// 约束具有容器功能的节点控件应该有什么方法 - /// - public interface INodeContainerControl - { - /// - /// 放置一个节点 - /// - /// - bool PlaceNode(NodeControlBase nodeControl); - - /// - /// 取出一个节点 - /// - /// - bool TakeOutNode(NodeControlBase nodeControl); - - /// - /// 取出所有节点(用于删除容器) - /// - void TakeOutAll(); - } -} diff --git a/Workbench/Node/INodeJunction.cs b/Workbench/Node/INodeJunction.cs deleted file mode 100644 index ae74d0d..0000000 --- a/Workbench/Node/INodeJunction.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Serein.Workbench.Node.View; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; - -namespace Serein.Workbench.Node -{ - - - - /// - /// 约束一个节点应该有哪些控制点 - /// - public interface INodeJunction - { - /// - /// 方法执行入口控制点 - /// - JunctionControlBase ExecuteJunction { get; } - /// - /// 执行完成后下一个要执行的方法控制点 - /// - JunctionControlBase NextStepJunction { get; } - - /// - /// 参数节点控制点 - /// - JunctionControlBase[] ArgDataJunction { get; } - /// - /// 返回值控制点 - /// - JunctionControlBase ReturnDataJunction { get; } - - /// - /// 获取目标参数控制点,用于防止wpf释放资源导致找不到目标节点,返回-1,-1的坐标 - /// - /// - /// - JunctionControlBase GetJunctionOfArgData(int index) - { - var arr = ArgDataJunction; - if (index >= arr.Length) - { - return null; - } - return arr[index]; - } - } -} diff --git a/Workbench/Node/Junction/ConnectionLineShape.cs b/Workbench/Node/Junction/ConnectionLineShape.cs deleted file mode 100644 index 122adaf..0000000 --- a/Workbench/Node/Junction/ConnectionLineShape.cs +++ /dev/null @@ -1,234 +0,0 @@ -using Serein.Library; -using Serein.Workbench.Extension; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Shapes; - -namespace Serein.Workbench.Node.View -{ - /// - /// 连接线的类型 - /// - public enum LineType - { - /// - /// 贝塞尔曲线 - /// - Bezier, - /// - /// 半圆线 - /// - Semicircle, - } - - - - /// - /// 贝塞尔曲线 - /// - public class ConnectionLineShape : Shape - { - private readonly double strokeThickness; - - private readonly LineType lineType; - - /// - /// 确定起始坐标和目标坐标、外光样式的曲线 - /// - /// 线条类型 - /// 起始坐标 - /// 结束坐标 - /// 颜色 - /// 是否为虚线 - public ConnectionLineShape(LineType lineType, - Point start, - Point end, - Brush brush, - bool isDotted = false, - bool isTop = false) - { - this.lineType = lineType; - this.brush = brush; - startPoint = start; - endPoint = end; - this.strokeThickness = 4; - InitElementPoint(isDotted, isTop); - InvalidateVisual(); // 触发重绘 - } - - - public void InitElementPoint(bool isDotted , bool isTop = false) - { - hitVisiblePen = new Pen(Brushes.Transparent, 1.0); // 初始化碰撞检测线 - hitVisiblePen.Freeze(); // Freeze以提高性能 - visualPen = new Pen(brush, 3.0); // 默认可视化Pen - opacity = 1.0d; - if (isDotted) - { - opacity = 0.42d; - visualPen.DashStyle = DashStyles.Dash; // 选择虚线样式 - } - visualPen.Freeze(); // Freeze以提高性能 - - linkSize = 4; // 整线条粗细 - int zIndex = -999999; - if (isTop) - { - zIndex *= -1; - } - Panel.SetZIndex(this, zIndex); // 置底 - } - - /// - /// 更新线条落点位置 - /// - /// - /// - public void UpdatePoints(Point start, Point end) - { - startPoint = start; - endPoint = end; - InvalidateVisual(); // 触发重绘 - } - - /// - /// 更新线条落点位置 - /// - /// - public void UpdateEndPoints(Point point) - { - endPoint = point; - InvalidateVisual(); // 触发重绘 - } - /// - /// 更新线条落点位置 - /// - /// - public void UpdateStartPoints(Point point) - { - startPoint = point; - InvalidateVisual(); // 触发重绘 - } - - /// - /// 控件重绘事件 - /// - /// - protected override void OnRender(DrawingContext drawingContext) - { - // 刷新线条显示位置 - switch (this.lineType) - { - case LineType.Bezier: - DrawBezierCurve(drawingContext, startPoint, endPoint); - break; - case LineType.Semicircle: - DrawSemicircleCurve(drawingContext, startPoint, endPoint); - break; - default: - break; - } - - } - #region 重绘 - - private readonly StreamGeometry streamGeometry = new StreamGeometry(); - private Point rightCenterOfStartLocation; // 目标节点选择左侧边缘中心 - private Point leftCenterOfEndLocation; // 起始节点选择右侧边缘中心 - private Pen hitVisiblePen; // 初始化碰撞检测线 - private Pen visualPen; // 默认可视化Pen - private Point startPoint; // 连接线的起始节点 - private Point endPoint; // 连接线的终点 - private Brush brush; // 线条颜色 - private double opacity; // 透明度 - - double linkSize; // 根据缩放比例调整线条粗细 - protected override Geometry DefiningGeometry => streamGeometry; - - public void UpdateLineColor(Brush brush) - { - visualPen = new Pen(brush, 3.0); // 默认可视化Pen - InvalidateVisual(); // 触发重绘 - } - - - private Point c0, c1; // 用于计算贝塞尔曲线控制点逻辑 - private Vector axis = new Vector(1, 0); - private Vector startToEnd; - private void DrawBezierCurve(DrawingContext drawingContext, - Point start, - Point end) - { - // 控制点的计算逻辑 - double power = 140; // 控制贝塞尔曲线的“拉伸”强度 - drawingContext.PushOpacity(opacity); - // 计算轴向向量与起点到终点的向量 - //var axis = new Vector(1, 0); - startToEnd = (end.ToVector() - start.ToVector()).NormalizeTo(); - - // 计算拉伸程度k,拉伸与水平夹角正相关 - var k = 1 - Math.Pow(Math.Max(0, axis.DotProduct(startToEnd)), 10.0); - - // 如果起点x大于终点x,增加额外的偏移量,避免重叠 - var bias = start.X > end.X ? Math.Abs(start.X - end.X) * 0.25 : 0; - - // 控制点的实际计算 - c0 = new Point(+(power + bias) * k + start.X, start.Y); - c1 = new Point(-(power + bias) * k + end.X, end.Y); - - // 准备StreamGeometry以用于绘制曲线 - streamGeometry.Clear(); - using (var context = streamGeometry.Open()) - { - context.BeginFigure(start, true, false); // 曲线起点 - context.BezierTo(c0, c1, end, true, false); // 画贝塞尔曲线 - } - drawingContext.DrawGeometry(null, visualPen, streamGeometry); - - } - - - - private void DrawSemicircleCurve(DrawingContext drawingContext, Point start, Point end) - { - // 计算中心点和半径 - // 计算圆心和半径 - double x = 35; - // 创建一个弧线路径 - streamGeometry.Clear(); - using (var context = streamGeometry.Open()) - { - // 开始绘制 - context.BeginFigure(start, false, false); - - // 生成弧线 - context.ArcTo( - end, // 结束点 - new Size(x, x), // 椭圆的半径 - 0, // 椭圆的旋转角度 - false, // 是否大弧 - SweepDirection.Counterclockwise, // 方向 - true, // 是否连接到起始点 - true // 是否使用高质量渲染 - ); - - // 结束绘制 - context.LineTo(start, false, false); // 连接到起始点(可选) - } - - // 绘制弧线 - drawingContext.DrawGeometry(null, visualPen, streamGeometry); - - } - #endregion - } - - -} diff --git a/Workbench/Node/Junction/JunctionControlBase.cs b/Workbench/Node/Junction/JunctionControlBase.cs deleted file mode 100644 index 5ce870c..0000000 --- a/Workbench/Node/Junction/JunctionControlBase.cs +++ /dev/null @@ -1,378 +0,0 @@ -using Serein.Library; -using Serein.Library.Utils; -using System; -using System.Net; -using System.Reflection; -using System.Windows; -using Serein.Workbench.Extension; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Shapes; -using System.Windows.Media.Media3D; -using System.Windows.Documents; -using System.Threading; - -namespace Serein.Workbench.Node.View -{ - internal static class MyUIFunc - { - public static Pen CreateAndFreezePen() - { - // 创建Pen - Pen pen = new Pen(Brushes.Black, 1); - - // 冻结Pen - if (pen.CanFreeze) - { - pen.Freeze(); - } - return pen; - } - } - - public class ParamsArgControl: Shape - { - - - public ParamsArgControl() - { - this.MouseDown += ParamsArg_OnMouseDown; // 增加或删除 - this.MouseMove += ParamsArgControl_MouseMove; - this.MouseLeave += ParamsArgControl_MouseLeave; - AddOrRemoveParamsTask = AddAsync; - } - - - - protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); - protected override Geometry DefiningGeometry => StreamGeometry; - - - #region 控件属性,所在的节点 - public static readonly DependencyProperty NodeProperty = - DependencyProperty.Register(nameof(MyNode), typeof(NodeModelBase), typeof(ParamsArgControl), new PropertyMetadata(default(NodeModelBase))); - //public NodeModelBase NodeModel; - - /// - /// 所在的节点 - /// - public NodeModelBase MyNode - { - get { return (NodeModelBase)GetValue(NodeProperty); } - set { SetValue(NodeProperty, value); } - } - #endregion - - #region 控件属性,连接器类型 - public static readonly DependencyProperty ArgIndexProperty = - DependencyProperty.Register(nameof(ArgIndex), typeof(int), typeof(ParamsArgControl), new PropertyMetadata(default(int))); - - /// - /// 参数的索引 - /// - public int ArgIndex - { - get { return (int)GetValue(ArgIndexProperty); } - set { SetValue(ArgIndexProperty, value.ToString()); } - } - #endregion - - - /// - /// 控件重绘事件 - /// - /// - protected override void OnRender(DrawingContext drawingContext) - { - Brush brush = isMouseOver ? Brushes.Red : Brushes.Green; - double height = ActualHeight; - // 定义圆形的大小和位置 - double connectorSize = 10; // 连接器的大小 - double circleCenterX = 8; // 圆心 X 坐标 - double circleCenterY = height / 2; // 圆心 Y 坐标 - var circlePoint = new Point(circleCenterX, circleCenterY); - - // 圆形部分 - var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - - drawingContext.DrawGeometry(brush, MyUIFunc.CreateAndFreezePen(), ellipse); - } - - - private bool isMouseOver; // 鼠标悬停状态 - - private Func AddOrRemoveParamsTask; // 增加或删除参数 - - public async void ParamsArg_OnMouseDown(object sender, MouseButtonEventArgs e) - { - await AddOrRemoveParamsTask.Invoke(); - } - - private void ParamsArgControl_MouseMove(object sender, MouseEventArgs e) - { - isMouseOver = true; - if (cancellationTokenSource.IsCancellationRequested) { - cancellationTokenSource = new CancellationTokenSource(); - Task.Run(async () => - { - await Task.Delay(380); - - }, cancellationTokenSource.Token).ContinueWith((t) => - { - // 如果焦点仍在控件上时,则改变点击事件 - if (isMouseOver) - { - AddOrRemoveParamsTask = RemoveAsync; - this.Dispatcher.Invoke(InvalidateVisual);// 触发一次重绘 - - } - }); - } - - } - private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - - private void ParamsArgControl_MouseLeave(object sender, MouseEventArgs e) - { - isMouseOver = false; - AddOrRemoveParamsTask = AddAsync; // 鼠标焦点离开时恢复点击事件 - cancellationTokenSource?.Cancel(); - this.Dispatcher.Invoke(InvalidateVisual);// 触发一次重绘 - - } - - - private async Task AddAsync() - { - await this.MyNode.Env.ChangeParameter(MyNode.Guid, true, ArgIndex); - } - private async Task RemoveAsync() - { - await this.MyNode.Env.ChangeParameter(MyNode.Guid, false, ArgIndex); - } - - } - - - - public abstract class JunctionControlBase : Shape - { - protected JunctionControlBase() - { - this.Width = 25; - this.Height = 20; - this.MouseDown += JunctionControlBase_MouseDown; - this.MouseMove += JunctionControlBase_MouseMove; - this.MouseLeave += JunctionControlBase_MouseLeave; ; - } - - - #region 控件属性,所在的节点 - public static readonly DependencyProperty NodeProperty = - DependencyProperty.Register(nameof(MyNode), typeof(NodeModelBase), typeof(JunctionControlBase), new PropertyMetadata(default(NodeModelBase))); - //public NodeModelBase NodeModel; - - /// - /// 所在的节点 - /// - public NodeModelBase MyNode - { - get { return (NodeModelBase)GetValue(NodeProperty); } - set { SetValue(NodeProperty, value); } - } - #endregion - - #region 控件属性,连接器类型 - public static readonly DependencyProperty JunctionTypeProperty = - DependencyProperty.Register(nameof(JunctionType), typeof(string), typeof(JunctionControlBase), new PropertyMetadata(default(string))); - - /// - /// 控制点类型 - /// - public JunctionType JunctionType - { - get { return EnumHelper.ConvertEnum(GetValue(JunctionTypeProperty).ToString()); } - set { SetValue(JunctionTypeProperty, value.ToString()); } - } - #endregion - - protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); - protected override Geometry DefiningGeometry => StreamGeometry; - - /// - /// 重绘方法 - /// - /// - public abstract void Render(DrawingContext drawingContext); - /// - /// 中心点 - /// - public abstract Point MyCenterPoint { get; } - - - - /// - /// 禁止连接 - /// - private bool IsConnectionDisable; - - /// - /// 处理鼠标悬停状态 - /// - private bool _isMouseOver; - public bool IsMouseOver - { - get => _isMouseOver; - set - { - if(_isMouseOver != value) - { - GlobalJunctionData.MyGlobalConnectingData.CurrentJunction = this; - _isMouseOver = value; - InvalidateVisual(); - } - - } - } - - /// - /// 控件重绘事件 - /// - /// - protected override void OnRender(DrawingContext drawingContext) - { - Render(drawingContext); - } - - /// - /// 获取背景颜色 - /// - /// - protected Brush GetBackgrounp() - { - var myData = GlobalJunctionData.MyGlobalConnectingData; - if(!myData.IsCreateing) - { - return Brushes.Transparent; - } - if (IsMouseOver) - { - if (myData.IsCanConnected) - { - if (myData.Type == JunctionOfConnectionType.Invoke) - { - return myData.ConnectionInvokeType.ToLineColor(); - } - else - { - return myData.ConnectionArgSourceType.ToLineColor(); - } - } - else - { - return Brushes.Red; - } - } - else - { - return Brushes.Transparent; - } - } - - private object lockObj = new object(); - - /// - /// 控件获得鼠标焦点事件 - /// - /// - /// - private void JunctionControlBase_MouseMove(object sender, MouseEventArgs e) - { - //if (!GlobalJunctionData.MyGlobalConnectingData.IsCreateing) return; - - //if (IsMouseOver) return; - IsMouseOver = true; - - //this.InvalidateVisual(); - } - - /// - /// 控件失去鼠标焦点事件 - /// - /// - /// - private void JunctionControlBase_MouseLeave(object sender, MouseEventArgs e) - { - IsMouseOver = false; - e.Handled = true; - - } - - - /// - /// 在碰撞点上按下鼠标控件开始进行移动 - /// - /// - /// - protected void JunctionControlBase_MouseDown(object sender, MouseButtonEventArgs e) - { - if (e.LeftButton == MouseButtonState.Pressed) - { - var canvas = MainWindow.GetParentOfType(this); - if (canvas != null) - { - var myData = GlobalJunctionData.MyGlobalConnectingData; - myData.Reset(); - myData.IsCreateing = true; // 表示开始连接 - myData.StartJunction = this; - myData.CurrentJunction = this; - myData.StartPoint = this.TranslatePoint(new Point(this.Width / 2, this.Height / 2), canvas); - - var junctionOfConnectionType = this.JunctionType.ToConnectyionType(); - ConnectionLineShape bezierLine; // 类别 - Brush brushColor; // 临时线的颜色 - if (junctionOfConnectionType == JunctionOfConnectionType.Invoke) - { - brushColor = ConnectionInvokeType.IsSucceed.ToLineColor(); - } - else if(junctionOfConnectionType == JunctionOfConnectionType.Arg) - { - brushColor = ConnectionArgSourceType.GetOtherNodeData.ToLineColor(); - } - else - { - return; - } - bezierLine = new ConnectionLineShape(LineType.Bezier, - myData.StartPoint, - myData.StartPoint, - brushColor, - isTop: true); // 绘制临时的线 - - Mouse.OverrideCursor = Cursors.Cross; // 设置鼠标为正在创建连线 - myData.MyLine = new MyLine(canvas, bezierLine); - } - } - e.Handled = true; - } - - private Point GetStartPoint() - { - return new Point(this.ActualWidth / 2, this.ActualHeight / 2); // 起始节点选择右侧边缘中心 - } - - - - - - } - - - - - - - -} diff --git a/Workbench/Node/Junction/JunctionData.cs b/Workbench/Node/Junction/JunctionData.cs deleted file mode 100644 index ceb3048..0000000 --- a/Workbench/Node/Junction/JunctionData.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Serein.Library; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Shapes; - -namespace Serein.Workbench.Node.View -{ - - #region Model,不科学的全局变量 - public class MyLine - { - public MyLine(Canvas canvas, ConnectionLineShape line) - { - Canvas = canvas; - Line = line; - canvas?.Children.Add(line); - } - - public Canvas Canvas { get; set; } - public ConnectionLineShape Line { get; set; } - - public void Remove() - { - Canvas?.Children.Remove(Line); - } - } - - public class ConnectingData - { - - /// - /// 是否正在创建连线 - /// - public bool IsCreateing { get; set; } - /// - /// 起始控制点 - /// - public JunctionControlBase StartJunction { get; set; } - /// - /// 当前的控制点 - /// - public JunctionControlBase CurrentJunction { get; set; } - /// - /// 开始坐标 - /// - public Point StartPoint { get; set; } - /// - /// 线条样式 - /// - public MyLine MyLine { get; set; } - - /// - /// 线条类别(方法调用) - /// - public ConnectionInvokeType ConnectionInvokeType { get; set; } = ConnectionInvokeType.IsSucceed; - /// - /// 线条类别(参数传递) - /// - public ConnectionArgSourceType ConnectionArgSourceType { get; set; } = ConnectionArgSourceType.GetOtherNodeData; - - /// - /// 判断当前连接类型 - /// - public JunctionOfConnectionType Type => StartJunction.JunctionType.ToConnectyionType(); - - - /// - /// 是否允许连接 - /// - - public bool IsCanConnected { get - { - - if(StartJunction is null - || CurrentJunction is null - ) - { - return false; - } - - - if (!StartJunction.MyNode.Equals(CurrentJunction.MyNode) - && StartJunction.JunctionType.IsCanConnection(CurrentJunction.JunctionType)) - { - return true; - } - else - { - return false; - } - } - } - - /// - /// 更新临时的连接线 - /// - /// - public void UpdatePoint(Point point) - { - if (StartJunction is null - || CurrentJunction is null - ) - { - return; - } - if (StartJunction.JunctionType == Library.JunctionType.Execute - || StartJunction.JunctionType == Library.JunctionType.ArgData) - { - MyLine.Line.UpdateStartPoints(point); - } - else - { - MyLine.Line.UpdateEndPoints(point); - - } - } - - /// - /// 重置 - /// - public void Reset() - { - IsCreateing = false; - StartJunction = null; - CurrentJunction = null; - MyLine?.Remove(); - ConnectionInvokeType = ConnectionInvokeType.IsSucceed; - ConnectionArgSourceType = ConnectionArgSourceType.GetOtherNodeData; - } - - - - } - - public static class GlobalJunctionData - { - //private static ConnectingData? myGlobalData; - //private static object _lockObj = new object(); - - /// - /// 创建节点之间控制点的连接行为 - /// - public static ConnectingData MyGlobalConnectingData { get; } = new ConnectingData(); - - /// - /// 删除连接视觉效果 - /// - public static void OK() - { - MyGlobalConnectingData.Reset(); - } - } - #endregion -} diff --git a/Workbench/Node/Junction/NodeJunctionViewBase.cs b/Workbench/Node/Junction/NodeJunctionViewBase.cs deleted file mode 100644 index 92e6a59..0000000 --- a/Workbench/Node/Junction/NodeJunctionViewBase.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Media; -using System.Windows; -using Serein.Workbench.Node.View; -using System.Windows.Controls; -using Serein.Library; -using System.Windows.Data; - -namespace Serein.Workbench.Node.View -{ - - - public abstract class NodeJunctionViewBase : ContentControl, IDisposable - { - public NodeJunctionViewBase() - { - var transfromGroup = new TransformGroup(); - transfromGroup.Children.Add(_Translate); - RenderTransform = transfromGroup; - } - - /// - /// 每个连接器都有一个唯一标识符(Guid),用于标识连接器。 - /// - public Guid Guid - { - get => (Guid)GetValue(GuidProperty); - set => SetValue(GuidProperty, value); - } - public static readonly DependencyProperty GuidProperty = DependencyProperty.Register( - nameof(Guid), - typeof(Guid), - typeof(NodeJunctionViewBase), // NodeConnectorContent - new PropertyMetadata(Guid.Empty)); - - /// - /// 连接器当前的连接数,表示有多少条 NodeLink 连接到此连接器。该属性为只读。 - /// - public int ConnectedCount - { - get => (int)GetValue(ConnectedCountProperty); - private set => SetValue(ConnectedCountPropertyKey, value); - } - public static readonly DependencyPropertyKey ConnectedCountPropertyKey = DependencyProperty.RegisterReadOnly( - nameof(ConnectedCount), - typeof(int), - typeof(NodeJunctionViewBase), // NodeConnectorContent - new PropertyMetadata(0)); - - public static readonly DependencyProperty ConnectedCountProperty = ConnectedCountPropertyKey.DependencyProperty; - - /// - /// 布尔值,指示此连接器是否有任何连接。 - /// - public bool IsConnected - { - get => (bool)GetValue(IsConnectedProperty); - private set => SetValue(IsConnectedPropertyKey, value); - } - public static readonly DependencyPropertyKey IsConnectedPropertyKey = DependencyProperty.RegisterReadOnly( - nameof(IsConnected), - typeof(bool), - typeof(NodeJunctionViewBase), // NodeConnectorContent - new PropertyMetadata(false)); - - public static readonly DependencyProperty IsConnectedProperty = IsConnectedPropertyKey.DependencyProperty; - - /// - /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 - /// - public Brush Stroke - { - get => (Brush)GetValue(StrokeProperty); - set => SetValue(StrokeProperty, value); - } - public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register( - nameof(Stroke), - typeof(Brush), - typeof(NodeJunctionViewBase), // NodeConnectorContent - new FrameworkPropertyMetadata(Brushes.Blue)); - - /// - /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 - /// - public double StrokeThickness - { - get => (double)GetValue(StrokeThicknessProperty); - set => SetValue(StrokeThicknessProperty, value); - } - public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register( - nameof(StrokeThickness), - typeof(double), - typeof(NodeJunctionViewBase), // NodeConnectorContent - new FrameworkPropertyMetadata(1.0)); - - /// - /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 - /// - public Brush Fill - { - get => (Brush)GetValue(FillProperty); - set => SetValue(FillProperty, value); - } - public static readonly DependencyProperty FillProperty = DependencyProperty.Register( - nameof(Fill), - typeof(Brush), - typeof(NodeJunctionViewBase),// NodeConnectorContent - new FrameworkPropertyMetadata(Brushes.Gray)); - - /// - /// 指示该连接器是否可以与其他连接器进行连接。 - /// - public bool CanConnect - { - get => (bool)GetValue(CanConnectProperty); - set => SetValue(CanConnectProperty, value); - } - public static readonly DependencyProperty CanConnectProperty = DependencyProperty.Register( - nameof(CanConnect), - typeof(bool), - typeof(NodeJunctionViewBase),// NodeConnectorContent - new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender)); - - - private Point _Position = new Point(); - /// - /// 该连接器的当前坐标(位置)。 - /// - public Point Position - { - get => _Position; - set => UpdatePosition(value); - } - - /// - /// (重要数据)表示连接器所属的节点。 - /// - public NodeModelBase NodeModel { get; private set; } = null; - - /// - /// 该连接器所连接的所有 NodeLink 的集合。 - /// - public IEnumerable NodeLinks => _NodeLinks; - List _NodeLinks = new List(); - - protected abstract FrameworkElement ConnectorControl { get; } - TranslateTransform _Translate = new TranslateTransform(); - void UpdatePosition(Point pos) - { - _Position = pos; - _Translate.X = _Position.X; - _Translate.Y = _Position.Y; - - InvalidateVisual(); - } - - /// - /// 将 NodeLink 添加到连接器,并更新 ConnectedCount 和 IsConnected。 - /// - /// - public void Connect(ConnectionControl nodeLink) - { - _NodeLinks.Add(nodeLink); - ConnectedCount = _NodeLinks.Count; - IsConnected = ConnectedCount > 0; - } - - /// - /// 断开与某个 NodeLink 的连接,更新连接状态。 - /// - /// - public void Disconnect(ConnectionControl nodeLink) - { - _NodeLinks.Remove(nodeLink); - ConnectedCount = _NodeLinks.Count; - IsConnected = ConnectedCount > 0; - } - - /// - /// 获取连接器相对于指定 Canvas 的位置。 - /// - /// - /// - /// - /// - public Point GetContentPosition(Canvas canvas, double xScaleOffset = 0.5, double yScaleOffset = 0.5) - { - // it will be shifted Control position if not called UpdateLayout(). - ConnectorControl.UpdateLayout(); - var transformer = ConnectorControl.TransformToVisual(canvas); - - var x = ConnectorControl.ActualWidth * xScaleOffset; - var y = ConnectorControl.ActualHeight * yScaleOffset; - return transformer.Transform(new Point(x, y)); - } - - /// - /// 更新与此连接器相连的所有 NodeLink 的位置。这个方法是抽象的,要求子类实现。 - /// - /// - public abstract void UpdateLinkPosition(Canvas canvas); - - /// - /// 用于检查此连接器是否可以与另一个连接器相连接,要求子类实现。 - /// - /// - /// - public abstract bool CanConnectTo(NodeJunctionViewBase connector); - - /// - /// 释放连接器相关的资源,包括样式、绑定和已连接的 NodeLink - /// - public void Dispose() - { - // You need to clear Style. - // Because implemented on style for binding. - Style = null; - - // Clear binding for subscribing source changed event from old control. - // throw exception about visual tree ancestor different if you not clear binding. - BindingOperations.ClearAllBindings(this); - - var nodeLinks = _NodeLinks.ToArray(); - - // it must instance to nodeLinks because change node link collection in NodeLink Dispose. - foreach (var nodeLink in nodeLinks) - { - // nodeLink.Dispose(); - } - } - - } -} diff --git a/Workbench/Node/Junction/View/ArgJunctionControl.cs b/Workbench/Node/Junction/View/ArgJunctionControl.cs deleted file mode 100644 index 6ef6927..0000000 --- a/Workbench/Node/Junction/View/ArgJunctionControl.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using System.Windows.Shapes; -using Serein.Library; - -namespace Serein.Workbench.Node.View -{ - public class ArgJunctionControl : JunctionControlBase - { - public ArgJunctionControl() - { - base.JunctionType = JunctionType.ArgData; - this.InvalidateVisual(); - } - - #region 控件属性,对应的参数 - public static readonly DependencyProperty ArgIndexProperty = - DependencyProperty.Register("ArgIndex", typeof(int), typeof(ArgJunctionControl), new PropertyMetadata(default(int))); - - /// - /// 所在的节点 - /// - public int ArgIndex - { - get { return (int)GetValue(ArgIndexProperty); } - set { SetValue(ArgIndexProperty, value); } - } - - - #endregion - private Point _myCenterPoint; - public override Point MyCenterPoint { get => _myCenterPoint; } - - - public override void Render(DrawingContext drawingContext) - { - double width = ActualWidth; - double height = ActualHeight; - var background = GetBackgrounp(); - // 输入连接器的背景 - var connectorRect = new Rect(0, 0, width, height); - drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); - - // 定义圆形的大小和位置 - double connectorSize = 10; // 连接器的大小 - double circleCenterX = 8; // 圆心 X 坐标 - double circleCenterY = height / 2; // 圆心 Y 坐标 - var circlePoint = new Point(circleCenterX, circleCenterY); - _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 - - // 绘制连接器的圆形部分 - var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); - - // 定义三角形的间距 - double triangleOffsetX = 4; // 三角形与圆形的间距 - double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 - double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 - - // 绘制三角形 - var pathGeometry = new StreamGeometry(); - using (var context = pathGeometry.Open()) - { - context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); - context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); - } - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); - } - } - - -} diff --git a/Workbench/Node/Junction/View/ExecuteJunctionControl.cs b/Workbench/Node/Junction/View/ExecuteJunctionControl.cs deleted file mode 100644 index 9247df1..0000000 --- a/Workbench/Node/Junction/View/ExecuteJunctionControl.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Shapes; -using Serein.Library; - -namespace Serein.Workbench.Node.View -{ - public class ExecuteJunctionControl : JunctionControlBase - { - - - - public ExecuteJunctionControl() - { - base.JunctionType = JunctionType.Execute; - this.InvalidateVisual(); - - } - private Point _myCenterPoint; - public override Point MyCenterPoint { get => _myCenterPoint; } - public override void Render(DrawingContext drawingContext) - { - double width = ActualWidth; - double height = ActualHeight; - var background = GetBackgrounp(); - // 绘制边框 - //var borderBrush = new SolidColorBrush(Colors.Black); - //var borderThickness = 1.0; - //var borderRect = new Rect(0, 0, width, height); - //drawingContext.DrawRectangle(null, new Pen(borderBrush, borderThickness), borderRect); - - // 输入连接器的背景 - var connectorRect = new Rect(0, 0, width, height); - drawingContext.DrawRectangle(Brushes.Transparent,null, connectorRect); - //drawingContext.DrawRectangle(Brushes.Transparent, new Pen(background,2), connectorRect); - - // 定义圆形的大小和位置 - double connectorSize = 10; // 连接器的大小 - double circleCenterX = 8; // 圆心 X 坐标 - double circleCenterY = height / 2; // 圆心 Y 坐标 - _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 - - var circlePoint = new Point(circleCenterX, circleCenterY); - // 绘制连接器的圆形部分 - var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - - - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); - - - - - // 定义三角形的间距 - double triangleOffsetX = 4; // 三角形与圆形的间距 - double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 - double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 - - // 绘制三角形 - var pathGeometry = new StreamGeometry(); - using (var context = pathGeometry.Open()) - { - context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); - context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); - } - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); - - // 绘制标签 - //var formattedText = new FormattedText( - // "执行", - // System.Globalization.CultureInfo.CurrentCulture, - // FlowDirection.LeftToRight, - // new Typeface("Segoe UI"), - // 12, - // Brushes.Black, - // VisualTreeHelper.GetDpi(this).PixelsPerDip); - //drawingContext.DrawText(formattedText, new Point(18,1)); - } - } - - -} diff --git a/Workbench/Node/Junction/View/NextStepJunctionControl.cs b/Workbench/Node/Junction/View/NextStepJunctionControl.cs deleted file mode 100644 index 7c5bde9..0000000 --- a/Workbench/Node/Junction/View/NextStepJunctionControl.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using System.Windows.Shapes; -using Serein.Library; - -namespace Serein.Workbench.Node.View -{ - - public class NextStepJunctionControl : JunctionControlBase - { - //public override JunctionType JunctionType { get; } = JunctionType.NextStep; - public NextStepJunctionControl() - { - base.JunctionType = JunctionType.NextStep; - this.InvalidateVisual(); - } - private Point _myCenterPoint; - public override Point MyCenterPoint { get => _myCenterPoint; } - public override void Render(DrawingContext drawingContext) - { - double width = ActualWidth; - double height = ActualHeight; - var background = GetBackgrounp(); - // 输入连接器的背景 - var connectorRect = new Rect(0, 0, width, height); - drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); - - // 定义圆形的大小和位置 - double connectorSize = 10; // 连接器的大小 - double circleCenterX = 8; // 圆心 X 坐标 - double circleCenterY = height / 2; // 圆心 Y 坐标 - _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 - - var circlePoint = new Point(circleCenterX, circleCenterY); - // 绘制连接器的圆形部分 - var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); - - // 绘制连接器的圆形部分 - //var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - - - // 定义三角形的间距 - double triangleOffsetX = 4; // 三角形与圆形的间距 - double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 - double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 - - // 绘制三角形 - var pathGeometry = new StreamGeometry(); - using (var context = pathGeometry.Open()) - { - context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); - context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); - } - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); - } - } -} diff --git a/Workbench/Node/Junction/View/ResultJunctionControl.cs b/Workbench/Node/Junction/View/ResultJunctionControl.cs deleted file mode 100644 index aaad0d3..0000000 --- a/Workbench/Node/Junction/View/ResultJunctionControl.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Windows; -using System.Windows.Media; -using System.Windows.Shapes; -using Serein.Library; - -namespace Serein.Workbench.Node.View -{ - - public class ResultJunctionControl : JunctionControlBase - { - //public override JunctionType JunctionType { get; } = JunctionType.ReturnData; - - public ResultJunctionControl() - { - base.JunctionType = JunctionType.ReturnData; - this.InvalidateVisual(); - } - private Point _myCenterPoint; - public override Point MyCenterPoint { get => _myCenterPoint; } - - public override void Render(DrawingContext drawingContext) - { - double width = ActualWidth; - double height = ActualHeight; - - // 输入连接器的背景 - var connectorRect = new Rect(0, 0, width, height); - drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); - - var background = GetBackgrounp(); - - // 定义圆形的大小和位置 - double connectorSize = 10; // 连接器的大小 - double circleCenterX = 8; // 圆心 X 坐标 - double circleCenterY = height / 2; // 圆心 Y 坐标 - var circlePoint = new Point(circleCenterX, circleCenterY); - - _myCenterPoint = new Point(circleCenterX - connectorSize / 2 , circleCenterY); // 中心坐标 - - // 绘制连接器的圆形部分 - var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); - - // 定义三角形的间距 - double triangleOffsetX = 4; // 三角形与圆形的间距 - double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 - double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 - - // 绘制三角形 - var pathGeometry = new StreamGeometry(); - using (var context = pathGeometry.Open()) - { - context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); - context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); - context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); - } - drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); - } - } -} diff --git a/Workbench/Node/NodeControlBase.cs b/Workbench/Node/NodeControlBase.cs deleted file mode 100644 index 2487cfb..0000000 --- a/Workbench/Node/NodeControlBase.cs +++ /dev/null @@ -1,198 +0,0 @@ -using Serein.Library; -using Serein.Library.Api; -using Serein.Workbench.Node.ViewModel; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Input; -using System.Windows.Media; - -namespace Serein.Workbench.Node.View -{ - - /// - /// 节点控件基类(控件) - /// - public abstract class NodeControlBase : UserControl, IDynamicFlowNode - { - /// - /// 节点所在的画布(以后需要将画布封装出来,实现多画布的功能) - /// - public Canvas NodeCanvas { get; set; } - - private INodeContainerControl nodeContainerControl; - /// - /// 如果该节点放置在了某个容器节点,就会记录这个容器节点 - /// - private INodeContainerControl NodeContainerControl { get; } - - /// - /// 记录与该节点控件有关的所有连接 - /// - private readonly List connectionControls = new List(); - - public NodeControlViewModelBase ViewModel { get; set; } - - - protected NodeControlBase() - { - this.Background = Brushes.Transparent; - } - - protected NodeControlBase(NodeControlViewModelBase viewModelBase) - { - ViewModel = viewModelBase; - this.Background = Brushes.Transparent; - this.DataContext = viewModelBase; - SetBinding(); - } - - /// - /// 放置在某个节点容器中 - /// - public void PlaceToContainer(INodeContainerControl nodeContainerControl) - { - this.nodeContainerControl = nodeContainerControl; - NodeCanvas.Children.Remove(this); // 临时从画布上移除 - var result = nodeContainerControl.PlaceNode(this); - if (!result) // 检查是否放置成功,如果不成功,需要重新添加回来 - { - NodeCanvas.Children.Add(this); // 从画布上移除 - - } - } - - /// - /// 从某个节点容器取出 - /// - public void TakeOutContainer() - { - var result = nodeContainerControl.TakeOutNode(this); // 从控件取出 - if (result) // 移除成功时才添加到画布上 - { - NodeCanvas.Children.Add(this); // 重新添加到画布上 - if (nodeContainerControl is NodeControlBase containerControl) - { - this.ViewModel.NodeModel.Position.X = containerControl.ViewModel.NodeModel.Position.X + containerControl.Width + 10; - this.ViewModel.NodeModel.Position.Y = containerControl.ViewModel.NodeModel.Position.Y; - } - } - - } - - /// - /// 添加与该节点有关的连接后,记录下来 - /// - /// - public void AddCnnection(ConnectionControl connection) - { - connectionControls.Add(connection); - } - - /// - /// 删除了连接之后,还需要从节点中的记录移除 - /// - /// - public void RemoveConnection(ConnectionControl connection) - { - connectionControls.Remove(connection); - connection.Remote(); - } - - /// - /// 删除所有连接 - /// - public void RemoveAllConection() - { - foreach (var connection in this.connectionControls) - { - connection.Remote(); - } - } - - /// - /// 更新与该节点有关的数据 - /// - public void UpdateLocationConnections() - { - foreach (var connection in this.connectionControls) - { - connection.RefreshLine(); // 主动更新连线位置 - } - } - - - /// - /// 设置绑定: - /// Canvas.X and Y : 画布位置 - /// - public void SetBinding() - { - // 绑定 Canvas.Left - Binding leftBinding = new Binding("X") - { - Source = ViewModel.NodeModel.Position, // 如果 X 属性在当前 DataContext 中 - Mode = BindingMode.TwoWay - }; - BindingOperations.SetBinding(this, Canvas.LeftProperty, leftBinding); - - // 绑定 Canvas.Top - Binding topBinding = new Binding("Y") - { - Source = ViewModel.NodeModel.Position, // 如果 Y 属性在当前 DataContext 中 - Mode = BindingMode.TwoWay - }; - BindingOperations.SetBinding(this, Canvas.TopProperty, topBinding); - } - - /// - /// 穿透视觉树获取指定类型的第一个元素 - /// - /// - /// - /// - protected T FindVisualChild(DependencyObject parent) where T : DependencyObject - { - for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) - { - var child = VisualTreeHelper.GetChild(parent, i); - if (child is T typedChild) - { - return typedChild; - } - - var childOfChild = FindVisualChild(child); - if (childOfChild != null) - { - return childOfChild; - } - } - return null; - } - - - - } - - - - - //public class FLowNodeObObservableCollection : ObservableCollection - //{ - - // public void AddRange(IEnumerable items) - // { - // foreach (var item in items) - // { - // this.Items.Add(item); - // } - // OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add)); - // } - //} -} - - - - - - diff --git a/Workbench/Node/RelayCommand.cs b/Workbench/Node/RelayCommand.cs deleted file mode 100644 index a19b531..0000000 --- a/Workbench/Node/RelayCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows.Input; - -namespace Serein.Workbench.Node -{ - - public class RelayCommand : ICommand - { - private readonly Action _execute; - private readonly Func _canExecute; - - public RelayCommand(Action execute, Func canExecute = null) - { - _execute = execute; - _canExecute = canExecute; - } - - public event EventHandler CanExecuteChanged; - - public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); - - public void Execute(object parameter) => _execute(parameter); - - public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); - } - -} diff --git a/Workbench/Node/View/ConnectionControl.cs b/Workbench/Node/View/ConnectionControl.cs deleted file mode 100644 index e41637d..0000000 --- a/Workbench/Node/View/ConnectionControl.cs +++ /dev/null @@ -1,297 +0,0 @@ -using Serein.Library; -using Serein.Library.Api; -using Serein.Workbench.Extension; -using System; -using System.Net; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Shapes; -using Color = System.Windows.Media.Color; -using ColorConverter = System.Windows.Media.ColorConverter; -using Point = System.Windows.Point; - -namespace Serein.Workbench.Node.View -{ - #region 连接点相关代码 - - - - public class ConnectionModelBase - { - /// - /// 起始节点 - /// - public NodeModelBase StartNode { get; set; } - /// - /// 目标节点 - /// - public NodeModelBase EndNode { get; set; } - - /// - /// 来源于起始节点的(控制点)类型 - /// - public JunctionType JoinTypeOfStart { get; set; } - - /// - /// 连接到目标节点的(控制点)类型 - /// - public JunctionType JoinTypeOfEnd { get; set; } - - /// - /// 连接类型 - /// - public ConnectionInvokeType Type { get; set; } - } - - - public interface IJunctionNode - { - string BoundNodeGuid { get; } - } - - /// - /// 连接点 - /// - public class JunctionNode : IJunctionNode - { - /// - /// 连接点类型 - /// - public JunctionType JunctionType { get; } - /// - /// 对应的视图对象 - /// - public NodeModelBase NodeModel { get; set; } - /// - /// - /// - public string BoundNodeGuid { get => NodeModel.Guid; } - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #endregion - - /// - /// 连接控件,表示控件的连接关系 - /// - public class ConnectionControl - { - /// - /// 所在的画布 - /// - public Canvas Canvas { get; } - - /// - /// 调用方法类型,连接类型 - /// - public ConnectionInvokeType InvokeType { get; } - - /// - /// 目标节点控制点 - /// - private INodeJunction EndNode; - - /// - /// 获取参数类型,第几个参数 - /// - public int ArgIndex { get; set; } = -1; - - /// - /// 参数来源(决定了连接线的样式) - /// - public ConnectionArgSourceType ArgSourceType { get; set; } - - /// - /// 起始控制点 - /// - public JunctionControlBase Start { get; set; } - - /// - /// 目标控制点 - /// - public JunctionControlBase End { get; set; } - - /// - /// 连接线 - /// - private ConnectionLineShape BezierLine; - - - - private LineType LineType; - - /// - /// 关于调用 - /// - /// - /// - public ConnectionControl(Canvas Canvas, - ConnectionInvokeType invokeType, - JunctionControlBase Start, - JunctionControlBase End) - { - this.LineType = LineType.Bezier; - this.Canvas = Canvas; - this.InvokeType = invokeType; - this.Start = Start; - this.End = End; - InitElementPoint(); - } - - /// - /// 关于入参 - /// - /// - /// - public ConnectionControl(LineType LineType, - Canvas Canvas, - int argIndex, - ConnectionArgSourceType argSourceType, - JunctionControlBase Start, - JunctionControlBase End, - INodeJunction nodeJunction) - { - this.LineType = LineType; - this.Canvas = Canvas; - this.ArgIndex = argIndex; - this.ArgSourceType = argSourceType; - this.Start = Start; - this.End = End; - this.EndNode = nodeJunction; - InitElementPoint(); - } - - /// - /// 绘制 - /// - public void InitElementPoint() - { - leftCenterOfEndLocation = Start.MyCenterPoint; - rightCenterOfStartLocation = End.MyCenterPoint; - - (Point startPoint, Point endPoint) = RefreshPoint(Canvas, Start, End); - var connectionType = Start.JunctionType.ToConnectyionType(); - bool isDotted; - Brush brush; - if(connectionType == JunctionOfConnectionType.Invoke) - { - brush = InvokeType.ToLineColor(); - isDotted = false; - } - else - { - brush = ArgSourceType.ToLineColor(); - isDotted = true; // 如果为参数,则绘制虚线 - } - BezierLine = new ConnectionLineShape(LineType, startPoint, endPoint, brush, isDotted); - Grid.SetZIndex(BezierLine, -9999999); // 置底 - Canvas.Children.Add(BezierLine); - - ConfigureLineContextMenu(); //配置右键菜单 - } - - - /// - /// 配置连接曲线的右键菜单 - /// - private void ConfigureLineContextMenu() - { - var contextMenu = new ContextMenu(); - contextMenu.Items.Add(MainWindow.CreateMenuItem("删除连线", (s, e) => Remote())); - contextMenu.Items.Add(MainWindow.CreateMenuItem("于父节点调用顺序中置顶", (s, e) => Topping())); - BezierLine.ContextMenu = contextMenu; - } - - - /// - /// 删除该连线 - /// - public void Remote() - { - Canvas.Children.Remove(BezierLine); - var env = Start.MyNode.Env; - if (Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke) - { - env.RemoveConnectInvokeAsync(Start.MyNode.Guid, End.MyNode.Guid, InvokeType); - } - else if (Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Arg) - { - env.RemoveConnectArgSourceAsync(Start.MyNode.Guid, End.MyNode.Guid, ArgIndex) ; - } - } - - /// - /// 置顶调用关系 - /// - public void Topping() - { - var env = Start.MyNode.Env; - if (Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke) - { - env.SetConnectPriorityInvoke(Start.MyNode.Guid, End.MyNode.Guid, InvokeType); - } - } - - /// - /// 重新绘制 - /// - public void RefreshLine() - { - if(ArgIndex > -1) - { - End = EndNode.GetJunctionOfArgData(ArgIndex) ?? End; - } - (Point startPoint, Point endPoint) = RefreshPoint(Canvas, Start, End); - BezierLine.UpdatePoints(startPoint, endPoint); - } - - - private Point rightCenterOfStartLocation; // 目标节点选择左侧边缘中心 - private Point leftCenterOfEndLocation; // 起始节点选择右侧边缘中心 - /// - /// 刷新坐标 - /// - - private (Point startPoint, Point endPoint) RefreshPoint(Canvas canvas, FrameworkElement startElement, FrameworkElement endElement) - { - var startPoint = startElement.TranslatePoint(rightCenterOfStartLocation, canvas); // 获取起始节点的中心位置 - var endPoint = endElement.TranslatePoint(leftCenterOfEndLocation, canvas); // 计算终点位置 - return (startPoint, endPoint); - } - } - - - - - - - - -} diff --git a/Workbench/Node/View/GlobalDataControl.xaml b/Workbench/Node/View/GlobalDataControl.xaml deleted file mode 100644 index 56b23c3..0000000 --- a/Workbench/Node/View/GlobalDataControl.xaml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Workbench/Node/View/GlobalDataControl.xaml.cs b/Workbench/Node/View/GlobalDataControl.xaml.cs deleted file mode 100644 index f3f4bf3..0000000 --- a/Workbench/Node/View/GlobalDataControl.xaml.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.ViewModel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace Serein.Workbench.Node.View -{ - /// - /// UserControl1.xaml 的交互逻辑 - /// - public partial class GlobalDataControl : NodeControlBase, INodeJunction, INodeContainerControl - { - public GlobalDataControl() : base() - { - // 窗体初始化需要 - base.ViewModel = new GlobalDataNodeControlViewModel(new SingleGlobalDataNode(null)); - DataContext = ViewModel; - InitializeComponent(); - } - - public GlobalDataControl(GlobalDataNodeControlViewModel viewModel) : base(viewModel) - { - DataContext = viewModel; - InitializeComponent(); - } - - - /// - /// 入参控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; - - /// - /// 下一个调用方法控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; - - /// - /// 返回值控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ReturnDataJunction => throw new NotImplementedException(); - - /// - /// 方法入参控制点(可能有,可能没) - /// - JunctionControlBase[] INodeJunction.ArgDataJunction => throw new NotImplementedException(); - - - public bool PlaceNode(NodeControlBase nodeControl) - { - if (GlobalDataPanel.Children.Contains(nodeControl)) - { - return false; - } - GlobalDataPanel.Children.Add(nodeControl); - return true; - } - - public bool TakeOutNode(NodeControlBase nodeControl) - { - if (!GlobalDataPanel.Children.Contains(nodeControl)) - { - return false; - } - GlobalDataPanel.Children.Remove(nodeControl); - return true; - } - - public void TakeOutAll() - { - GlobalDataPanel.Children.Clear(); - } - - } -} diff --git a/Workbench/Node/View/ScriptNodeControl.xaml b/Workbench/Node/View/ScriptNodeControl.xaml deleted file mode 100644 index b7d0808..0000000 --- a/Workbench/Node/View/ScriptNodeControl.xaml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Workbench/Node/View/ScriptNodeControl.xaml.cs b/Workbench/Node/View/ScriptNodeControl.xaml.cs deleted file mode 100644 index f16decf..0000000 --- a/Workbench/Node/View/ScriptNodeControl.xaml.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.ViewModel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using System.Windows.Threading; - -namespace Serein.Workbench.Node.View -{ - /// - /// ScriptNodeControl.xaml 的交互逻辑 - /// - public partial class ScriptNodeControl : NodeControlBase , INodeJunction - { - private ScriptNodeControlViewModel viewModel => (ScriptNodeControlViewModel)ViewModel; - private DispatcherTimer _debounceTimer; // 用于延迟更新 - private bool _isUpdating = false; // 防止重复更新 - - public ScriptNodeControl() - { - InitializeComponent(); - } - public ScriptNodeControl(ScriptNodeControlViewModel viewModel) : base(viewModel) - { - DataContext = viewModel; - InitializeComponent(); - -#if false - // 初始化定时器 - _debounceTimer = new DispatcherTimer(); - _debounceTimer.Interval = TimeSpan.FromMilliseconds(500); // 停止输入 500ms 后更新 - _debounceTimer.Tick += DebounceTimer_Tick; -#endif - } - - - - - /// - /// 入参控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; - - /// - /// 下一个调用方法控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; - - /// - /// 返回值控制点(可能有,可能没) - /// - JunctionControlBase INodeJunction.ReturnDataJunction => this.ResultJunctionControl; - - /// - /// 方法入参控制点(可能有,可能没) - /// - JunctionControlBase[] INodeJunction.ArgDataJunction - { - get - { - // 获取 MethodDetailsControl 实例 - var methodDetailsControl = this.MethodDetailsControl; - var itemsControl = FindVisualChild(methodDetailsControl); // 查找 ItemsControl - if (itemsControl != null) - { - var argDataJunction = new JunctionControlBase[base.ViewModel.NodeModel.MethodDetails.ParameterDetailss.Length]; - var controls = new List(); - - for (int i = 0; i < itemsControl.Items.Count; i++) - { - var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement; - if (container != null) - { - var argControl = FindVisualChild(container); - if (argControl != null) - { - controls.Add(argControl); // 收集 ArgJunctionControl 实例 - } - } - } - return argDataJunction = controls.ToArray(); - } - else - { - return []; - } - } - - - } - - - - - - - - - - - - - -#if false - // 每次输入时重置定时器 - private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e) - { - _debounceTimer.Stop(); - _debounceTimer.Start(); - } - - // 定时器事件,用户停止输入后触发 - private async void DebounceTimer_Tick(object sender, EventArgs e) - { - _debounceTimer.Stop(); - - if (_isUpdating) - return; - - // 开始后台处理语法分析和高亮 - _isUpdating = true; - await Task.Run(() => HighlightKeywordsAsync(viewModel.Script)); - } - - // 异步执行语法高亮操作 - private async Task HighlightKeywordsAsync(string text) - { - if (string.IsNullOrEmpty(text)) - { - return; - } - // 模拟语法分析和高亮(可以替换为实际逻辑) - var highlightedText = text; - - // 在 UI 线程中更新 RichTextBox 的内容 - await Dispatcher.BeginInvoke(() => - { - var range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd); - range.Text = highlightedText; - }); - - _isUpdating = false; - } - -#endif - - - - - } -} diff --git a/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs b/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs deleted file mode 100644 index d9aaa08..0000000 --- a/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.View; - -namespace Serein.Workbench.Node.ViewModel -{ - public class ExpOpNodeControlViewModel: NodeControlViewModelBase - { - public new SingleExpOpNode NodeModel { get; } - - //public string Expression - //{ - // get => node.Expression; - // set - // { - // node.Expression = value; - // OnPropertyChanged(); - // } - //} - - - public ExpOpNodeControlViewModel(SingleExpOpNode nodeModel) : base(nodeModel) - { - this.NodeModel = nodeModel; - } - } -} diff --git a/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs b/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs deleted file mode 100644 index a396cba..0000000 --- a/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Serein.Library; -using Serein.NodeFlow.Model; -using Serein.Workbench.Node.View; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Input; - -namespace Serein.Workbench.Node.ViewModel -{ - public class GlobalDataNodeControlViewModel : NodeControlViewModelBase - { - private SingleGlobalDataNode NodeModel => (SingleGlobalDataNode)base.NodeModel; - - /// - /// 复制全局数据表达式 - /// - public ICommand CommandCopyDataExp { get; } - - /// - /// 刷新数据 - /// - public ICommand CommandRefreshData { get; } - - - public GlobalDataNodeControlViewModel(SingleGlobalDataNode node) : base(node) - { - CommandCopyDataExp = new RelayCommand( o => - { - string exp = NodeModel.KeyName; - string copyValue = $"@Get #{exp}#"; - Clipboard.SetDataObject(copyValue); - }); - } - - /// - /// 自定义参数值 - /// - public string? KeyName - { - get => NodeModel?.KeyName; - set { NodeModel.KeyName = value; OnPropertyChanged(); } - } - - - - } -} diff --git a/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs b/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs deleted file mode 100644 index 6b03113..0000000 --- a/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Serein.Library; -using Serein.Library.Core; -using Serein.Library.Utils; -using Serein.NodeFlow.Model; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Input; - -namespace Serein.Workbench.Node.ViewModel -{ - public class ScriptNodeControlViewModel : NodeControlViewModelBase - { - private SingleScriptNode NodeModel => (SingleScriptNode)base.NodeModel; - - public string? Script - { - get => NodeModel?.Script; - set { NodeModel.Script = value; OnPropertyChanged(); } - } - - - - public ScriptNodeControlViewModel(NodeModelBase nodeModel) : base(nodeModel) - { - CommandExecuting = new RelayCommand(async o => - { - try - { - var result = await NodeModel.ExecutingAsync(new DynamicContext(nodeModel.Env)); - SereinEnv.WriteLine(InfoType.INFO, result?.ToString()); - } - catch (Exception ex) - { - SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); - } - }); - - CommandLoadScript = new RelayCommand( o => - { - NodeModel.ReloadScript(); - }); - } - - - /// - /// 加载脚本代码 - /// - public ICommand CommandLoadScript{ get; } - - /// - /// 尝试执行 - /// - public ICommand CommandExecuting { get; } - - - - } -} diff --git a/Workbench/Properties/launchSettings.json b/Workbench/Properties/launchSettings.json deleted file mode 100644 index f8eb472..0000000 --- a/Workbench/Properties/launchSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "profiles": { - "Serein.Workbench": { - "commandName": "Project" - } - } -} \ No newline at end of file diff --git a/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj b/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj deleted file mode 100644 index 4810142..0000000 --- a/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj +++ /dev/null @@ -1,292 +0,0 @@ - - - Serein.Workbench - obj\Release\ - obj\ - D:\Project\C#\DynamicControl\SereinFlow\WorkBench\obj\ - <_TargetAssemblyProjectName>Serein.Workbench - Serein.Workbench - - - - WinExe - net8.0-windows - enable - enable - True - D:\Project\C#\DynamicControl\SereinFlow\.Output - MIT - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Code - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Workbench/Themes/BindableRichTextBox.cs b/Workbench/Themes/BindableRichTextBox.cs deleted file mode 100644 index f7c9d1e..0000000 --- a/Workbench/Themes/BindableRichTextBox.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Controls; -using System.Windows.Documents; -using System.Windows; - -namespace Serein.Workbench.Themes -{ - public partial class BindableRichTextBox : RichTextBox - { - public new FlowDocument Document - { - get { return (FlowDocument)GetValue(DocumentProperty); } - set { SetValue(DocumentProperty, value); } - } - // Using a DependencyProperty as the backing store for Document. This enables animation, styling, binding, etc... - public static readonly DependencyProperty DocumentProperty = - DependencyProperty.Register("Document", typeof(FlowDocument), typeof(BindableRichTextBox), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnDucumentChanged))); - private static void OnDucumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - RichTextBox rtb = (RichTextBox)d; - rtb.Document = (FlowDocument)e.NewValue; - } - } -} diff --git a/Workbench/Tool/GuidReplacer.cs b/Workbench/Tool/GuidReplacer.cs deleted file mode 100644 index eb2cf77..0000000 --- a/Workbench/Tool/GuidReplacer.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Serein.Workbench.Tool -{ - /// - /// Guid替换工具类 - /// - public class GuidReplacer - { - private class TrieNode - { - public Dictionary Children = new(); - public string Replacement; // 替换后的值 - } - - private readonly TrieNode _root = new(); - - // 构建字典树 - public void AddReplacement(string guid, string replacement) - { - var current = _root; - foreach (var c in guid) - { - if (!current.Children.ContainsKey(c)) - { - current.Children[c] = new TrieNode(); - } - current = current.Children[c]; - } - current.Replacement = replacement; - } - - // 替换逻辑 - public string Replace(string input) - { - var result = new StringBuilder(); - var current = _root; - int i = 0; - - while (i < input.Length) - { - if (current.Children.ContainsKey(input[i])) - { - current = current.Children[input[i]]; - i++; - - if (current.Replacement != null) // 找到匹配 - { - result.Append(current.Replacement); - current = _root; // 回到根节点 - } - } - else - { - result.Append(input[i]); - current = _root; // 未匹配,回到根节点 - i++; - } - } - return result.ToString(); - } - } - -} From 702af587f9bcb88de4f27159e35b8afce6f85f38 Mon Sep 17 00:00:00 2001 From: fengjiayi <12821976+ning_xi@user.noreply.gitee.com> Date: Sat, 4 Jan 2025 22:20:01 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E4=BA=86=E5=88=9B=E5=BB=BA=E8=BF=9E=E7=BA=BF=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E8=83=BD=E5=A4=9F=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=88=90=E5=8A=9F=E5=90=8E=E7=9A=84=E5=A4=96?= =?UTF-8?q?=E8=A7=82=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Library/Extension/SereinExtension.cs | 20 +- .../Api/INodeContainerControl.cs | 32 ++ Serein.Workbench.Avalonia/Api/INodeControl.cs | 24 +- .../Node/ViewModels/ActionNodeViewModel.cs | 1 + .../Node/ViewModels/NodeViewModelBase.cs | 119 ++++++- .../Custom/Node/Views/ActionNodeView.axaml | 13 +- .../Custom/Node/Views/ActionNodeView.axaml.cs | 15 +- .../Custom/Node/Views/NodeControlBase.cs | 57 ++++ .../Custom/Views/ConnectionLineShape.cs | 75 +++-- .../Custom/Views/NodeConnectionLineView.cs | 279 ++++++++++++++++ .../Custom/Views/NodeContainerView.axaml.cs | 6 +- .../Custom/Views/NodeJunctionView.axaml.cs | 267 ++++++++++------ .../Views/ParameterDetailsInfoView.axaml | 18 +- .../LibraryMethodInfoDataTemplate.cs | 8 +- .../Model/ConnectingData.cs | 69 ++-- .../{MyLine.cs => NodeConnectionLine.cs} | 7 +- .../Serein.Workbench.Avalonia.csproj | 1 + .../Services/NodeOperationService.cs | 301 +++++++++++++++--- .../Views/MainView.axaml | 2 +- WorkBench/Node/View/ActionNodeControl.xaml.cs | 3 +- 20 files changed, 1040 insertions(+), 277 deletions(-) create mode 100644 Serein.Workbench.Avalonia/Api/INodeContainerControl.cs create mode 100644 Serein.Workbench.Avalonia/Custom/Node/Views/NodeControlBase.cs create mode 100644 Serein.Workbench.Avalonia/Custom/Views/NodeConnectionLineView.cs rename Serein.Workbench.Avalonia/Model/{MyLine.cs => NodeConnectionLine.cs} (83%) diff --git a/Library/Extension/SereinExtension.cs b/Library/Extension/SereinExtension.cs index 83fd8df..d04aca5 100644 --- a/Library/Extension/SereinExtension.cs +++ b/Library/Extension/SereinExtension.cs @@ -158,25 +158,7 @@ namespace Serein.Library || (start == JunctionType.ArgData && end == JunctionType.ReturnData); } - //var endType = end.ToConnectyionType(); - //if (startType != endType - // || startType == JunctionOfConnectionType.None - // || endType == JunctionOfConnectionType.None) - //{ - // return false; - //} - //else - //{ - // if (startType == JunctionOfConnectionType.Invoke) - // { - - // return end == JunctionType.NextStep; - // } - // else // if (startType == JunctionOfConnectionType.Arg) - // { - // return end == JunctionType.ReturnData; - // } - //} + } } diff --git a/Serein.Workbench.Avalonia/Api/INodeContainerControl.cs b/Serein.Workbench.Avalonia/Api/INodeContainerControl.cs new file mode 100644 index 0000000..b826be2 --- /dev/null +++ b/Serein.Workbench.Avalonia/Api/INodeContainerControl.cs @@ -0,0 +1,32 @@ +using Serein.Workbench.Avalonia.Custom.Node.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Avalonia.Api +{ + /// + /// 约束具有容器功能的节点控件应该有什么方法 + /// + public interface INodeContainerControl + { + /// + /// 放置一个节点 + /// + /// + bool PlaceNode(NodeControlBase nodeControl); + + /// + /// 取出一个节点 + /// + /// + bool TakeOutNode(NodeControlBase nodeControl); + + /// + /// 取出所有节点(用于删除容器) + /// + void TakeOutAll(); + } +} diff --git a/Serein.Workbench.Avalonia/Api/INodeControl.cs b/Serein.Workbench.Avalonia/Api/INodeControl.cs index ed1158f..06770d2 100644 --- a/Serein.Workbench.Avalonia/Api/INodeControl.cs +++ b/Serein.Workbench.Avalonia/Api/INodeControl.cs @@ -7,17 +7,17 @@ using System.Threading.Tasks; namespace Serein.Workbench.Avalonia.Api { - internal interface INodeControl - { - /// - /// 对应的节点实体 - /// - NodeModelBase NodeModelBase { get; } + //internal interface INodeControl + //{ + // /// + // /// 对应的节点实体 + // /// + // NodeModelBase NodeModelBase { get; } - /// - /// 初始化使用的方法,设置节点实体 - /// - /// - void SetNodeModel(NodeModelBase nodeModel); - } + // /// + // /// 初始化使用的方法,设置节点实体 + // /// + // /// + // void SetNodeModel(NodeModelBase nodeModel); + //} } diff --git a/Serein.Workbench.Avalonia/Custom/Node/ViewModels/ActionNodeViewModel.cs b/Serein.Workbench.Avalonia/Custom/Node/ViewModels/ActionNodeViewModel.cs index 0429f57..9ef41a3 100644 --- a/Serein.Workbench.Avalonia/Custom/Node/ViewModels/ActionNodeViewModel.cs +++ b/Serein.Workbench.Avalonia/Custom/Node/ViewModels/ActionNodeViewModel.cs @@ -15,6 +15,7 @@ namespace Serein.Workbench.Avalonia.Custom.Node.ViewModels [ObservableProperty] private SingleActionNode? nodeMoel; + internal override NodeModelBase NodeModelBase { get => NodeMoel ?? throw new NotImplementedException(); set => NodeMoel = (SingleActionNode)value; } diff --git a/Serein.Workbench.Avalonia/Custom/Node/ViewModels/NodeViewModelBase.cs b/Serein.Workbench.Avalonia/Custom/Node/ViewModels/NodeViewModelBase.cs index 27b878f..d55f24f 100644 --- a/Serein.Workbench.Avalonia/Custom/Node/ViewModels/NodeViewModelBase.cs +++ b/Serein.Workbench.Avalonia/Custom/Node/ViewModels/NodeViewModelBase.cs @@ -1,5 +1,10 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; using Serein.Library; +using Serein.Workbench.Avalonia.Api; +using Serein.Workbench.Avalonia.Custom.Node.Views; +using Serein.Workbench.Avalonia.Custom.Views; using Serein.Workbench.Avalonia.ViewModels; using System; using System.Collections.Generic; @@ -15,5 +20,117 @@ namespace Serein.Workbench.Avalonia.Custom.Node.ViewModels internal abstract class NodeViewModelBase : ViewModelBase { internal abstract NodeModelBase NodeModelBase { get; set; } + + private Canvas NodeCanvas; + + /// + /// 如果该节点放置在了某个容器节点,就会记录这个容器节点 + /// + private INodeContainerControl NodeContainerControl { get; } + + public NodeModelBase NodeModel { get; set; } + + /// + /// 记录与该节点控件有关的所有连接 + /// + private readonly List connectionControls = new List(); + + //public NodeControlViewModelBase ViewModel { get; set; } + + + + public void SetNodeModel(NodeModelBase nodeModel) => this.NodeModel = nodeModel; + + + /// + /// 添加与该节点有关的连接后,记录下来 + /// + /// + public void AddCnnection(NodeConnectionLineView connection) + { + connectionControls.Add(connection); + } + + /// + /// 删除了连接之后,还需要从节点中的记录移除 + /// + /// + public void RemoveConnection(NodeConnectionLineView connection) + { + connectionControls.Remove(connection); + //connection.Remote(); + } + + /// + /// 删除所有连接 + /// + public void RemoveAllConection() + { + foreach (var connection in this.connectionControls) + { + //connection.Remote(); + } + } + + /// + /// 更新与该节点有关的数据 + /// + public void UpdateLocationConnections() + { + foreach (var connection in this.connectionControls) + { + //connection.RefreshLine(); // 主动更新连线位置 + } + } + + + /// + /// 设置绑定: + /// Canvas.X and Y : 画布位置 + /// + public void SetBinding() + { + /* // 绑定 Canvas.Left + Binding leftBinding = new Binding("X") + { + Source = ViewModel.NodeModel.Position, // 如果 X 属性在当前 DataContext 中 + Mode = BindingMode.TwoWay + }; + BindingOperations.Apply(this, Canvas.LeftProperty, leftBinding); + + // 绑定 Canvas.Top + Binding topBinding = new Binding("Y") + { + Source = ViewModel.NodeModel.Position, // 如果 Y 属性在当前 DataContext 中 + Mode = BindingMode.TwoWay + }; + BindingOperations.SetBinding(this, Canvas.TopProperty, topBinding);*/ + } + + /// + /// 穿透视觉树获取指定类型的第一个元素 + /// + /// + /// + /// + //protected T FindVisualChild(DependencyObject parent) where T : DependencyObject + //{ + // for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + // { + // var child = VisualTreeHelper.GetChild(parent, i); + // if (child is T typedChild) + // { + // return typedChild; + // } + + // var childOfChild = FindVisualChild(child); + // if (childOfChild != null) + // { + // return childOfChild; + // } + // } + // return null; + //} + } } diff --git a/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml b/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml index e24738a..372387a 100644 --- a/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml +++ b/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml @@ -1,15 +1,16 @@ - @@ -17,7 +18,7 @@ - + @@ -42,8 +43,8 @@ - - + + - + diff --git a/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml.cs b/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml.cs index 099852e..c669790 100644 --- a/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml.cs +++ b/Serein.Workbench.Avalonia/Custom/Node/Views/ActionNodeView.axaml.cs @@ -2,25 +2,22 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Serein.Library; +using Serein.NodeFlow.Model; using Serein.Workbench.Avalonia.Api; using Serein.Workbench.Avalonia.Custom.Node.ViewModels; namespace Serein.Workbench.Avalonia.Custom.Node.Views; -public partial class ActionNodeView : UserControl, INodeControl +public partial class ActionNodeView : NodeControlBase { private ActionNodeViewModel _vm; + + public ActionNodeView() { InitializeComponent(); - _vm = App.GetService(); - DataContext = _vm; + //_vm = App.GetService(); + //DataContext = _vm; } - NodeModelBase INodeControl.NodeModelBase => _vm.NodeModelBase ?? throw new System.NotImplementedException(); // ڵ - - void INodeControl.SetNodeModel(NodeModelBase nodeModel) // ڵ - { - _vm.NodeModelBase = nodeModel; - } } \ No newline at end of file diff --git a/Serein.Workbench.Avalonia/Custom/Node/Views/NodeControlBase.cs b/Serein.Workbench.Avalonia/Custom/Node/Views/NodeControlBase.cs new file mode 100644 index 0000000..151799d --- /dev/null +++ b/Serein.Workbench.Avalonia/Custom/Node/Views/NodeControlBase.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media; +using Serein.Library; +using Serein.Workbench.Avalonia.Api; +using Serein.Workbench.Avalonia.Custom.Views; +using Serein.Workbench.Avalonia.Model; +using Serein.Workbench.Avalonia.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Avalonia.Custom.Node.Views +{ + public class NodeControlBase : UserControl + { + protected NodeControlBase() + { + this.Background = Brushes.Transparent; + } + + /// + /// 放置在某个节点容器中 + /// + public void PlaceToContainer(INodeContainerControl nodeContainerControl) + { + //this.nodeContainerControl = nodeContainerControl; + //NodeCanvas.Children.Remove(this); // 临时从画布上移除 + //var result = nodeContainerControl.PlaceNode(this); + //if (!result) // 检查是否放置成功,如果不成功,需要重新添加回来 + //{ + // NodeCanvas.Children.Add(this); // 从画布上移除 + + //} + } + + /// + /// 从某个节点容器取出 + /// + public void TakeOutContainer() + { + //var result = nodeContainerControl.TakeOutNode(this); // 从控件取出 + //if (result) // 移除成功时才添加到画布上 + //{ + // NodeCanvas.Children.Add(this); // 重新添加到画布上 + // if (nodeContainerControl is NodeControlBase containerControl) + // { + // NodeModel.Position.X = NodeModel.Position.X + containerControl.Width + 10; + // NodeModel.Position.Y = NodeModel.Position.Y; + // } + //} + + } + } +} diff --git a/Serein.Workbench.Avalonia/Custom/Views/ConnectionLineShape.cs b/Serein.Workbench.Avalonia/Custom/Views/ConnectionLineShape.cs index 60f822f..bf79df9 100644 --- a/Serein.Workbench.Avalonia/Custom/Views/ConnectionLineShape.cs +++ b/Serein.Workbench.Avalonia/Custom/Views/ConnectionLineShape.cs @@ -23,23 +23,22 @@ namespace Serein.Workbench.Avalonia.Custom.Views { private readonly double strokeThickness; - /// - /// 确定起始坐标和目标坐标、外光样式的曲线 + /// 确定起始坐标和目标坐标、外观样式的曲线 /// - /// 起始坐标 - /// 结束坐标 + /// 起始坐标 + /// 结束坐标 /// 颜色 /// 是否为虚线 - public ConnectionLineShape(Point start, - Point end, + public ConnectionLineShape(Point left, + Point right, Brush brush, bool isDotted = false, bool isTop = false) { this.brush = brush; - startPoint = start; - endPoint = end; + this.leftPoint = left; + this.rightPoint = right; this.strokeThickness = 4; InitElementPoint(isDotted, isTop); InvalidateVisual(); // 触发重绘 @@ -50,9 +49,10 @@ namespace Serein.Workbench.Avalonia.Custom.Views { //hitVisiblePen = new Pen(Brushes.Transparent, 1.0); // 初始化碰撞检测线 //hitVisiblePen.Freeze(); // Freeze以提高性能 + visualPen = new Pen(brush, 3.0); // 默认可视化Pen opacity = 1.0d; - var dashStyle = new DashStyle(); + //var dashStyle = new DashStyle(); if (isDotted) { @@ -71,31 +71,44 @@ namespace Serein.Workbench.Avalonia.Custom.Views /// /// 更新线条落点位置 /// - /// - /// - public void UpdatePoints(Point start, Point end) + /// + /// + public void UpdatePoint(Point left, Point right, Brush? brush = null) { - startPoint = start; - endPoint = end; + if(brush is not null) + { + visualPen = new Pen(brush, 3.0); // 默认可视化Pen + } + this.leftPoint = left; + this.rightPoint = right; InvalidateVisual(); // 触发重绘 } /// /// 更新线条落点位置 /// - /// - public void UpdateEndPoints(Point point) + /// + public void UpdateRightPoint(Point right, Brush? brush = null) { - endPoint = point; + if (brush is not null) + { + visualPen = new Pen(brush, 3.0); // 默认可视化Pen + } + this.rightPoint = right; InvalidateVisual(); // 触发重绘 } + /// /// 更新线条起点位置 /// - /// - public void UpdateStartPoints(Point point) + /// + public void UpdateLeftPoints(Point left, Brush? brush = null) { - startPoint = point; + if (brush is not null) + { + visualPen = new Pen(brush, 3.0); // 默认可视化Pen + } + this.leftPoint = left; InvalidateVisual(); // 触发重绘 } @@ -106,7 +119,7 @@ namespace Serein.Workbench.Avalonia.Custom.Views public override void Render(DrawingContext drawingContext) { // 刷新线条显示位置 - DrawBezierCurve(drawingContext, startPoint, endPoint); + DrawBezierCurve(drawingContext, leftPoint, rightPoint); } #region 重绘 @@ -116,8 +129,8 @@ namespace Serein.Workbench.Avalonia.Custom.Views private Point leftCenterOfEndLocation; // 起始节点选择右侧边缘中心 //private Pen hitVisiblePen; // 初始化碰撞检测线 private Pen visualPen; // 默认可视化Pen - private Point startPoint; // 连接线的起始节点 - private Point endPoint; // 连接线的终点 + private Point leftPoint; // 连接线的起始节点 + private Point rightPoint; // 连接线的终点 private Brush brush; // 线条颜色 private double opacity; // 透明度 @@ -135,8 +148,8 @@ namespace Serein.Workbench.Avalonia.Custom.Views private Vector startToEnd; private int i = 0; private void DrawBezierCurve(DrawingContext drawingContext, - Point start, - Point end) + Point left, + Point right) { // 控制点的计算逻辑 double power = 140; // 控制贝塞尔曲线的“拉伸”强度 @@ -144,7 +157,7 @@ namespace Serein.Workbench.Avalonia.Custom.Views // 计算轴向向量与起点到终点的向量 //var axis = new Vector(1, 0); - startToEnd = (end.ToVector() - start.ToVector()).NormalizeTo(); + startToEnd = (right.ToVector() - left.ToVector()).NormalizeTo(); @@ -163,10 +176,10 @@ namespace Serein.Workbench.Avalonia.Custom.Views pow = pow > 0 ? 0 : pow; var k = 1 - pow; // 如果起点x大于终点x,增加额外的偏移量,避免重叠 - var bias = start.X > end.X ? Math.Abs(start.X - end.X) * 0.25 : 0; + var bias = left.X > right.X ? Math.Abs(left.X - right.X) * 0.25 : 0; // 控制点的实际计算 - c0 = new Point(+(power + bias) * k + start.X, start.Y); - c1 = new Point(-(power + bias) * k + end.X, end.Y); + c0 = new Point(+(power + bias) * k + left.X, left.Y); + c1 = new Point(-(power + bias) * k + right.X, right.Y); // 准备StreamGeometry以用于绘制曲线 // why can't clearValue()? @@ -204,8 +217,8 @@ namespace Serein.Workbench.Avalonia.Custom.Views // streamGeometry.ClearValue("AvaloniaProperty"); using (var context = streamGeometry.Open()) { - context.BeginFigure(start, true); // start point of the bezier-line - context.CubicBezierTo(c0, c1, end, true); // drawing bezier-line + context.BeginFigure(left, true); // start point of the bezier-line + context.CubicBezierTo(c0, c1, right, true); // drawing bezier-line } drawingContext.DrawGeometry(null, visualPen, streamGeometry); diff --git a/Serein.Workbench.Avalonia/Custom/Views/NodeConnectionLineView.cs b/Serein.Workbench.Avalonia/Custom/Views/NodeConnectionLineView.cs new file mode 100644 index 0000000..b491f2e --- /dev/null +++ b/Serein.Workbench.Avalonia/Custom/Views/NodeConnectionLineView.cs @@ -0,0 +1,279 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.VisualTree; +using Serein.Library; +using Serein.Script.Node; +using Serein.Workbench.Avalonia.Extension; +using Serein.Workbench.Avalonia.Services; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Color = Avalonia.Media.Color; +using Point = Avalonia.Point; + +namespace Serein.Workbench.Avalonia.Custom.Views +{ + + + + public class NodeConnectionLineView + { + /// + /// 线条类别(方法调用) + /// + public ConnectionInvokeType ConnectionInvokeType { get; set; } = ConnectionInvokeType.IsSucceed; + /// + /// 线条类别(参数传递) + /// + public ConnectionArgSourceType ConnectionArgSourceType { get; set; } = ConnectionArgSourceType.GetOtherNodeData; + + + /// + /// 画布 + /// + private Canvas Canvas; + /// + /// 连接线的起点 + /// + private NodeJunctionView? LeftNodeJunctionView; + /// + /// 连接线的终点 + /// + private NodeJunctionView? RightNodeJunctionView; + + /// + /// 连接时显示的线 + /// + public ConnectionLineShape? ConnectionLineShape { get; private set; } + + public NodeConnectionLineView(Canvas canvas, + NodeJunctionView? leftNodeJunctionView, + NodeJunctionView? rightNodeJunctionView) + { + this.Canvas = canvas; + this.LeftNodeJunctionView = leftNodeJunctionView; + this.RightNodeJunctionView = rightNodeJunctionView; + } + + /// + /// 连接到终点 + /// + /// + public void ToEnd(NodeJunctionView endNodeJunctionView) + { + if((endNodeJunctionView.JunctionType == JunctionType.NextStep + || endNodeJunctionView.JunctionType == JunctionType.ReturnData) + && RightNodeJunctionView is not null + /*&& LeftNodeJunctionView is null*/ + /*&& !LeftNodeJunctionView.Equals(endNodeJunctionView)*/) + { + LeftNodeJunctionView = endNodeJunctionView; + RefreshLineDsiplay(); + return; + } + else if ((endNodeJunctionView.JunctionType == JunctionType.Execute + || endNodeJunctionView.JunctionType == JunctionType.ArgData) + && LeftNodeJunctionView is not null + /*&& RightNodeJunctionView is null*/ + /*&& !RightNodeJunctionView.Equals(endNodeJunctionView)*/) + { + RightNodeJunctionView = endNodeJunctionView; + RefreshLineDsiplay(); + return; + } + + + // + + //var leftPoint = GetPoint(LeftNodeJunctionView); + //var rightPoint = GetPoint(RightNodeJunctionView); + //var brush = GetBackgrounp(); + //ConnectionLineShape.UpdatePoint(leftPoint, rightPoint); + //CreateLineShape(startPoint, endPoint, brush); + } + + /// + /// 刷新线的显示 + /// + public void RefreshLineDsiplay() + { + if(LeftNodeJunctionView is null || RightNodeJunctionView is null) + { + return; + } + var leftPoint = GetPoint(LeftNodeJunctionView); + var rightPoint = GetPoint(RightNodeJunctionView); + if (ConnectionLineShape is null) + { + Debug.WriteLine("创建"); + CreateLineShape(leftPoint, rightPoint, GetBackgrounp()); + } + else + { + Debug.WriteLine("刷新"); + var brush = GetBackgrounp(); + ConnectionLineShape.UpdatePoint( leftPoint, rightPoint, brush); + } + } + + + /// + /// 刷新临时线的显示 + /// + public void RefreshRightPointOfTempLineDsiplay(Point rightPoint) + { + if(ConnectionLineShape is not null) + { + var brush = GetBackgrounp(); + ConnectionLineShape.UpdateRightPoint(rightPoint, brush); + return; + } + + if (LeftNodeJunctionView is not null) + { + var leftPoint = GetPoint(LeftNodeJunctionView); + var brush = GetBackgrounp(); + CreateLineShape(leftPoint, rightPoint, brush); + } + } + /// + /// 刷新临时线的显示 + /// + public void RefreshLeftPointOfTempLineDsiplay(Point leftPoint) + { + if(ConnectionLineShape is not null) + { + var brush = GetBackgrounp(); + ConnectionLineShape.UpdateLeftPoints(leftPoint, brush); + return; + } + + if (RightNodeJunctionView is not null) + { + var rightPoint = GetPoint(RightNodeJunctionView); + var brush = GetBackgrounp(); + CreateLineShape(leftPoint, rightPoint, brush); + } + } + + + + private static Point defaultPoint = new Point(0, 0); + int count; + private Point GetPoint(NodeJunctionView nodeJunctionView) + { + + var junctionSize = nodeJunctionView.GetTransformedBounds()!.Value.Bounds.Size; + Point junctionPoint; + if (nodeJunctionView.JunctionType == JunctionType.ArgData || nodeJunctionView.JunctionType == JunctionType.Execute) + { + junctionPoint = new Point(junctionSize.Width / 2 - 11, junctionSize.Height / 2); // 选择左侧 + } + else + { + junctionPoint = new Point(junctionSize.Width / 2 + 11, junctionSize.Height / 2); // 选择右侧 + } + if (nodeJunctionView.TranslatePoint(junctionPoint, Canvas) is Point point) + { + //myData.StartPoint = point; + return point; + } + else + { + return defaultPoint; + } + + //var point = nodeJunctionView.TranslatePoint(defaultPoint , Canvas); + //if(point is null) + //{ + // return defaultPoint; + //} + //else + //{ + // return point.Value; + // } + } + + private void CreateLineShape(Point leftPoint, Point rightPoint, Brush brush) + { + ConnectionLineShape = new ConnectionLineShape(leftPoint, rightPoint, brush); + Canvas.Children.Add(ConnectionLineShape); + } + + private JunctionOfConnectionType GetConnectionType() + { + return LeftNodeJunctionView.JunctionType.ToConnectyionType(); + } + + + /// + /// 获取背景颜色 + /// + /// + public Brush GetBackgrounp() + { + + if(LeftNodeJunctionView is null || RightNodeJunctionView is null) + { + return new SolidColorBrush(Color.Parse("#FF0000")); // 没有终点 + } + + // 判断连接控制点是否匹配 + if (!IsCanConnected()) + { + return new SolidColorBrush(Color.Parse("#FF0000")); + } + + + if (GetConnectionType() == JunctionOfConnectionType.Invoke) + { + return ConnectionInvokeType.ToLineColor(); // 调用 + } + else + { + return ConnectionArgSourceType.ToLineColor(); // 参数 + } + + } + + public bool IsCanConnected() + { + if (LeftNodeJunctionView is null + || RightNodeJunctionView is null) + { + return false; + } + if (LeftNodeJunctionView?.MyNode is null + || LeftNodeJunctionView.MyNode.Equals(RightNodeJunctionView.MyNode)) + return false; + + if (LeftNodeJunctionView.JunctionType.IsCanConnection(RightNodeJunctionView.JunctionType)) + { + return true; + } + else + { + return false; + } + } + + /// + /// 移除线 + /// + public void Remove() + { + if(ConnectionLineShape is null) + { + return; + } + Canvas.Children.Remove(ConnectionLineShape); + } + } +} diff --git a/Serein.Workbench.Avalonia/Custom/Views/NodeContainerView.axaml.cs b/Serein.Workbench.Avalonia/Custom/Views/NodeContainerView.axaml.cs index 198a3f6..1c8e6d5 100644 --- a/Serein.Workbench.Avalonia/Custom/Views/NodeContainerView.axaml.cs +++ b/Serein.Workbench.Avalonia/Custom/Views/NodeContainerView.axaml.cs @@ -98,6 +98,7 @@ public partial class NodeContainerView : UserControl { IsCanvasDragging = false; IsControlDragging = false; + nodeOperationService.ConnectingData.Reset(); } }; #endregion @@ -190,12 +191,11 @@ public partial class NodeContainerView : UserControl private void NodeContainerView_PointerMoved(object? sender, PointerEventArgs e) { - + // Ƿ var myData = nodeOperationService.ConnectingData; if (myData.IsCreateing) { var isPass = e.JudgePointer(sender, PointerType.Mouse, p => p.IsLeftButtonPressed); - //Debug.WriteLine("canvas ispass = " + isPass); if (isPass) { if (myData.Type == JunctionOfConnectionType.Invoke) @@ -208,7 +208,9 @@ public partial class NodeContainerView : UserControl _vm.IsConnectionArgSourceNode = true; // ӽڵĵùϵ } var currentPoint = e.GetPosition(PART_NodeContainer); + //myData.CurrentJunction?.InvalidateVisual(); myData.UpdatePoint(new Point(currentPoint.X - 5, currentPoint.Y - 5)); + e.Handled = true; return; } diff --git a/Serein.Workbench.Avalonia/Custom/Views/NodeJunctionView.axaml.cs b/Serein.Workbench.Avalonia/Custom/Views/NodeJunctionView.axaml.cs index dbf6ee2..18fe1ff 100644 --- a/Serein.Workbench.Avalonia/Custom/Views/NodeJunctionView.axaml.cs +++ b/Serein.Workbench.Avalonia/Custom/Views/NodeJunctionView.axaml.cs @@ -1,18 +1,14 @@ using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Media; using Serein.Library; -using Serein.Workbench.Avalonia.Views; -using System.Drawing; +using Serein.Library.Api; +using Serein.Workbench.Avalonia.Api; +using Serein.Workbench.Avalonia.Extension; using System; using Color = Avalonia.Media.Color; using Point = Avalonia.Point; -using System.Diagnostics; -using Avalonia.Threading; -using Serein.Workbench.Avalonia.Api; -using Serein.Workbench.Avalonia.Extension; namespace Serein.Workbench.Avalonia.Custom.Views; @@ -21,55 +17,6 @@ namespace Serein.Workbench.Avalonia.Custom.Views; /// public class NodeJunctionView : TemplatedControl { - private readonly INodeOperationService nodeOperationService; - - /// - /// RenderпԻ - /// - protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); - - /// - /// ڲ鿴 - /// - private bool IsPreviewing; - - public NodeJunctionView() - { - nodeOperationService = App.GetService(); - this.PointerMoved += NodeJunctionView_PointerMoved; - this.PointerExited += NodeJunctionView_PointerExited; - - this.PointerPressed += NodeJunctionView_PointerPressed; - this.PointerReleased += NodeJunctionView_PointerReleased; - } - - private void NodeJunctionView_PointerReleased(object? sender, PointerReleasedEventArgs e) - { - nodeOperationService.ConnectingData.IsCreateing = false; - } - - private void NodeJunctionView_PointerPressed(object? sender, PointerPressedEventArgs e) - { - nodeOperationService.TryCreateConnectionOnJunction(this); // Կʼ - Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); - } - - - - /// - /// ȡؼϢ - /// - /// - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - //if (e.NameScope.Find("PART_FlipflopMethodInfos") is ListBox p_fm) - //{ - // //p_fm.SelectionChanged += ListBox_SelectionChanged; - // //p_fm.PointerExited += ListBox_PointerExited; - //} - } - public static readonly DirectProperty JunctionTypeProperty = AvaloniaProperty.RegisterDirect(nameof(JunctionType), o => o.JunctionType, (o, v) => o.JunctionType = v); @@ -88,6 +35,160 @@ public class NodeJunctionView : TemplatedControl get { return myNode; } set { SetAndRaise(MyNodeProperty, ref myNode, value); } } + + public static readonly DirectProperty ArgIndexProperty = + AvaloniaProperty.RegisterDirect(nameof(ArgIndex), o => o.ArgIndex, (o, v) => o.ArgIndex = v); + private int argIndex; + public int ArgIndex + { + get { return argIndex; } + set { SetAndRaise(ArgIndexProperty, ref argIndex, value); } + } + + + + private readonly INodeOperationService nodeOperationService; + private readonly IFlowEnvironment flowEnvironment; + + /// + /// RenderпԻ + /// + protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); + + + + #region ¼ + + + public NodeJunctionView() + { + nodeOperationService = App.GetService(); + flowEnvironment = App.GetService(); + //this.PointerExited += NodeJunctionView_PointerExited; + this.PointerMoved += NodeJunctionView_PointerMoved; + this.PointerPressed += NodeJunctionView_PointerPressed; + this.PointerReleased += NodeJunctionView_PointerReleased; + } + + + public bool IsPreviewing { get; set; } + private Guid Guid = Guid.NewGuid(); + + private void NodeJunctionView_PointerMoved(object? sender, PointerEventArgs e) + { + if (!nodeOperationService.ConnectingData.IsCreateing) + return; + if (nodeOperationService.MainCanvas is not InputElement inputElement) + return; + var currentPoint = e.GetPosition(nodeOperationService.MainCanvas); + if (inputElement.InputHitTest(currentPoint) is NodeJunctionView junctionView) + { + RefreshDisplay(junctionView); + } + else + { + var oldNj = nodeOperationService.ConnectingData.CurrentJunction; + if (oldNj is not null) + { + oldNj.IsPreviewing = false; + oldNj.InvalidateVisual(); + } + } + } + + private void RefreshDisplay(NodeJunctionView junctionView) + { + var oldNj = nodeOperationService.ConnectingData.CurrentJunction; + if (oldNj is not null ) + { + if (junctionView.Equals(oldNj)) + { + return; + } + oldNj.IsPreviewing = false; + oldNj.InvalidateVisual(); + } + nodeOperationService.ConnectingData.CurrentJunction = junctionView; + if (!this.Equals(junctionView)) + { + + nodeOperationService.ConnectingData.TempLine?.ToEnd(junctionView); + } + junctionView.IsPreviewing = true; + junctionView.InvalidateVisual(); + } + + + + /// + /// Կʼ + /// + /// + /// + private void NodeJunctionView_PointerPressed(object? sender, PointerPressedEventArgs e) + { + nodeOperationService.TryCreateConnectionOnJunction(this); // Կʼ + } + private void NodeJunctionView_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + CheckJunvtion(); + nodeOperationService.ConnectingData.Reset(); + } + + private void CheckJunvtion() + { + var myData = nodeOperationService.ConnectingData; + if(myData.StartJunction is null || myData.CurrentJunction is null) + { + return; + } + if(myData.StartJunction.MyNode is null || myData.CurrentJunction.MyNode is null) + { + return; + } + if (!myData.IsCanConnected()) + { + return; + } + + var canvas = nodeOperationService.MainCanvas; + + #region ùϵ + if (myData.Type == JunctionOfConnectionType.Invoke) + { + flowEnvironment.ConnectInvokeNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, + myData.StartJunction.JunctionType, + myData.CurrentJunction.JunctionType, + myData.ConnectionInvokeType); + } + #endregion + + #region Դϵ + else if (myData.Type == JunctionOfConnectionType.Arg) + { + var argIndex = 0; + if (myData.StartJunction.JunctionType == JunctionType.ArgData) + { + argIndex = myData.StartJunction.ArgIndex; + } + else if (myData.CurrentJunction.JunctionType == JunctionType.ArgData) + { + argIndex = myData.CurrentJunction.ArgIndex; + } + + flowEnvironment.ConnectArgSourceNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, + myData.StartJunction.JunctionType, + myData.CurrentJunction.JunctionType, + myData.ConnectionArgSourceType, + argIndex); + } + #endregion + + + } + + #endregion + #region ػUIӾ @@ -101,7 +202,8 @@ public class NodeJunctionView : TemplatedControl double width = 44; double height = 26; var background = GetBackgrounp(); - var pen = new Pen(Brushes.Black, 1); + var pen = new Pen(Brushes.Transparent, 1); + //var pen = nodeOperationService.ConnectingData.IsCreateing ? new Pen(background, 1) : new Pen(Brushes.Black, 1); // ı var connectorRect = new Rect(0, 0, width, height); @@ -132,32 +234,10 @@ public class NodeJunctionView : TemplatedControl context.LineTo(new Point(triangleCenterX, triangleCenterY + t), true); context.LineTo(new Point(triangleCenterX, triangleCenterY - t), true); } - drawingContext.DrawGeometry(background, new Pen(Brushes.Black, 1), pathGeometry); + drawingContext.DrawGeometry(background, pen, pathGeometry); } - #region ¼ - - private void NodeJunctionView_PointerExited(object? sender, PointerEventArgs e) - { - if (IsPreviewing) - { - IsPreviewing = false; - Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); - } - } - - private void NodeJunctionView_PointerMoved(object? sender, PointerEventArgs e) - { - if (!IsPreviewing) - { - IsPreviewing = true; - Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); - } - - } - - #endregion /// @@ -167,38 +247,25 @@ public class NodeJunctionView : TemplatedControl protected IBrush GetBackgrounp() { var myData = nodeOperationService.ConnectingData; - if (!myData.IsCreateing) + if (IsPreviewing == false || !myData.IsCreateing ) { - //Debug.WriteLine($"return color is {Brushes.BurlyWood}"); return new SolidColorBrush(Color.Parse("#76ABEE")); } - if (myData.IsCanConnected) + if (!myData.IsCanConnected()) { - if (myData.Type == JunctionOfConnectionType.Invoke) - { - return myData.ConnectionInvokeType.ToLineColor(); - } - else - { - return myData.ConnectionArgSourceType.ToLineColor(); - } - } - else - { - return Brushes.Red; + return new SolidColorBrush(Color.Parse("#FF0000")); } - if (IsPreviewing) + if (myData.Type == JunctionOfConnectionType.Invoke) { - //return new SolidColorBrush(Color.Parse("#04FC10")); - + return myData.ConnectionInvokeType.ToLineColor(); // } else { - //Debug.WriteLine($"return color is {Brushes.BurlyWood}"); - return new SolidColorBrush(Color.Parse("#76ABEE")); + return myData.ConnectionArgSourceType.ToLineColor(); // } + } #endregion diff --git a/Serein.Workbench.Avalonia/Custom/Views/ParameterDetailsInfoView.axaml b/Serein.Workbench.Avalonia/Custom/Views/ParameterDetailsInfoView.axaml index 785860c..f9b9064 100644 --- a/Serein.Workbench.Avalonia/Custom/Views/ParameterDetailsInfoView.axaml +++ b/Serein.Workbench.Avalonia/Custom/Views/ParameterDetailsInfoView.axaml @@ -22,7 +22,7 @@ - + - + + + - + @@ -45,12 +47,16 @@ - - + + + + MinWidth="120" MaxWidth="300" + HorizontalAlignment="Left" VerticalAlignment="Center"> diff --git a/Serein.Workbench.Avalonia/DataTemplates/LibraryMethodInfoDataTemplate.cs b/Serein.Workbench.Avalonia/DataTemplates/LibraryMethodInfoDataTemplate.cs index 0e0b883..b513421 100644 --- a/Serein.Workbench.Avalonia/DataTemplates/LibraryMethodInfoDataTemplate.cs +++ b/Serein.Workbench.Avalonia/DataTemplates/LibraryMethodInfoDataTemplate.cs @@ -30,12 +30,7 @@ namespace Serein.Workbench.Avalonia.DataTemplates textBlock.Margin = new Thickness(2d, -6d, 2d, -6d); textBlock.FontSize = 12; textBlock.PointerPressed += TextBlock_PointerPressed; - //var stackPanel = new StackPanel(); - //stackPanel.Children.Add(textBlock); - //ToolTip toolTip = new ToolTip(); - //toolTip.FontSize = 12; - //toolTip.Content = mdInfo.MethodAnotherName; - //textBlock.Tag = mdInfo; + textBlock.Tag = mdInfo; return textBlock; } else @@ -43,7 +38,6 @@ namespace Serein.Workbench.Avalonia.DataTemplates var textBlock = new TextBlock() { Text = $"Binding 类型不为预期的[MethodDetailsInfo],而是[{param?.GetType()}]" }; textBlock.Margin = new Thickness(2d, -6d, 2d, -6d); textBlock.FontSize = 12; - textBlock.PointerPressed += TextBlock_PointerPressed; return textBlock; } diff --git a/Serein.Workbench.Avalonia/Model/ConnectingData.cs b/Serein.Workbench.Avalonia/Model/ConnectingData.cs index 26e945f..24e63ea 100644 --- a/Serein.Workbench.Avalonia/Model/ConnectingData.cs +++ b/Serein.Workbench.Avalonia/Model/ConnectingData.cs @@ -1,8 +1,10 @@ using Avalonia; +using Avalonia.Threading; using Serein.Library; using Serein.Workbench.Avalonia.Custom.Views; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -34,7 +36,7 @@ namespace Serein.Workbench.Avalonia.Model /// /// 线条样式 /// - public MyLine? TempLine { get; set; } + public NodeConnectionLineView? TempLine { get; set; } /// /// 线条类别(方法调用) @@ -49,59 +51,56 @@ namespace Serein.Workbench.Avalonia.Model /// 判断当前连接类型 /// public JunctionOfConnectionType? Type => StartJunction?.JunctionType.ToConnectyionType(); - + /// /// 是否允许连接 /// - - public bool IsCanConnected + public bool IsCanConnected() { - get + if (StartJunction is null + || CurrentJunction is null ) { + return false; + } + if (StartJunction?.MyNode is null + || StartJunction.MyNode.Equals(CurrentJunction.MyNode)) + return false; - if (StartJunction is null - || CurrentJunction is null - ) - { - return false; - } - if(StartJunction?.MyNode is null) - { - return false; - } - if (!StartJunction.MyNode.Equals(CurrentJunction.MyNode) - && StartJunction.JunctionType.IsCanConnection(CurrentJunction.JunctionType)) - { - return true; - } - else - { - return false; - } + if (StartJunction.JunctionType.IsCanConnection(CurrentJunction.JunctionType)) + { + return true; + } + else + { + return false; } } + + /// /// 更新临时的连接线 /// /// public void UpdatePoint(Point point) { - if (StartJunction is null - || CurrentJunction is null - ) + if (StartJunction is null || CurrentJunction is null ) + { + return; + } + if (IsCanConnected()) { return; } if (StartJunction.JunctionType == Library.JunctionType.Execute || StartJunction.JunctionType == Library.JunctionType.ArgData) { - TempLine?.Line.UpdateStartPoints(point); + TempLine?.RefreshLeftPointOfTempLineDsiplay(point); } else { - TempLine?.Line.UpdateEndPoints(point); + TempLine?.RefreshRightPointOfTempLineDsiplay(point); } } @@ -111,9 +110,17 @@ namespace Serein.Workbench.Avalonia.Model /// public void Reset() { + if(CurrentJunction is not null) + { + CurrentJunction.IsPreviewing = false; + Dispatcher.UIThread.InvokeAsync(CurrentJunction.InvalidateVisual, DispatcherPriority.Background); + } + if(StartJunction is not null) + { + StartJunction.IsPreviewing = false; + Dispatcher.UIThread.InvokeAsync(StartJunction.InvalidateVisual, DispatcherPriority.Background); + } IsCreateing = false; - StartJunction = null; - CurrentJunction = null; TempLine?.Remove(); ConnectionInvokeType = ConnectionInvokeType.IsSucceed; ConnectionArgSourceType = ConnectionArgSourceType.GetOtherNodeData; diff --git a/Serein.Workbench.Avalonia/Model/MyLine.cs b/Serein.Workbench.Avalonia/Model/NodeConnectionLine.cs similarity index 83% rename from Serein.Workbench.Avalonia/Model/MyLine.cs rename to Serein.Workbench.Avalonia/Model/NodeConnectionLine.cs index d543755..7a0c85a 100644 --- a/Serein.Workbench.Avalonia/Model/MyLine.cs +++ b/Serein.Workbench.Avalonia/Model/NodeConnectionLine.cs @@ -12,20 +12,21 @@ namespace Serein.Workbench.Avalonia.Model /// /// 绘制的线 /// - public class MyLine + public class NodeConnectionLine { /// - /// 将线条绘制出来 + /// 将线条绘制出来(临时线) /// /// 放置画布 /// 线的实体 - public MyLine(Canvas canvas, ConnectionLineShape line) + public NodeConnectionLine(Canvas canvas, ConnectionLineShape line) { Canvas = canvas; Line = line; canvas?.Children.Add(line); } + public Canvas Canvas { get; } public ConnectionLineShape Line { get; } diff --git a/Serein.Workbench.Avalonia/Serein.Workbench.Avalonia.csproj b/Serein.Workbench.Avalonia/Serein.Workbench.Avalonia.csproj index ef5b5df..23d6ab0 100644 --- a/Serein.Workbench.Avalonia/Serein.Workbench.Avalonia.csproj +++ b/Serein.Workbench.Avalonia/Serein.Workbench.Avalonia.csproj @@ -19,6 +19,7 @@ + diff --git a/Serein.Workbench.Avalonia/Services/NodeOperationService.cs b/Serein.Workbench.Avalonia/Services/NodeOperationService.cs index 7608f82..e6079af 100644 --- a/Serein.Workbench.Avalonia/Services/NodeOperationService.cs +++ b/Serein.Workbench.Avalonia/Services/NodeOperationService.cs @@ -15,6 +15,7 @@ using Serein.Workbench.Avalonia.Extension; using Serein.Workbench.Avalonia.Model; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -81,12 +82,12 @@ namespace Serein.Workbench.Avalonia.Api internal class NodeViewCreateEventArgs : EventArgs { - internal NodeViewCreateEventArgs(INodeControl nodeControl, PositionOfUI position) + internal NodeViewCreateEventArgs(NodeControlBase nodeControl, PositionOfUI position) { this.NodeControl = nodeControl; this.Position = position; } - public INodeControl NodeControl { get; private set; } + public NodeControlBase NodeControl { get; private set; } public PositionOfUI Position { get; private set; } } @@ -112,11 +113,9 @@ namespace Serein.Workbench.Avalonia.Services { this.flowEnvironment = flowEnvironment; this.feefService = feefService; - - NodeMVVMManagement.RegisterUI(NodeControlType.Action, typeof(ActionNodeView), typeof(ActionNodeViewModel)); // 注册动作节点 - ConnectingData = new ConnectingData(); feefService.OnNodeCreate += FeefService_OnNodeCreate; // 订阅运行环境创建节点事件 - + feefService.OnNodeConnectChange += FeefService_OnNodeConnectChange; // 订阅运行环境连接了节点事件 + NodeMVVMManagement.RegisterUI(NodeControlType.Action, typeof(ActionNodeView), typeof(ActionNodeViewModel)); // 注册动作节点 // 手动加载项目 _ = Task.Run(async delegate @@ -133,17 +132,24 @@ namespace Serein.Workbench.Avalonia.Services } - public ConnectingData ConnectingData { get; private set; } + + #region 接口属性 + public ConnectingData ConnectingData { get; private set; } = new ConnectingData(); public Canvas MainCanvas { get; set; } - + #endregion #region 私有变量 /// /// 存储所有与节点有关的控件 /// - private Dictionary NodeControls { get; } = []; + private Dictionary NodeControls { get; } = []; + + /// + /// 存储所有连接 + /// + private List Connections { get; } = []; @@ -158,28 +164,16 @@ namespace Serein.Workbench.Avalonia.Services private readonly IFlowEEForwardingService feefService; #endregion + #region 节点操作事件 + /// /// 创建了节点控件 /// public event NodeViewCreateHandle OnNodeViewCreate; - /// - /// 创建节点控件 - /// - /// 控件类型 - /// 创建坐标 - /// 节点方法信息(基础节点传null) - public void CreateNodeView(MethodDetailsInfo methodDetailsInfo, PositionOfUI position) - { - Task.Run(async () => - { - if (EnumHelper.TryConvertEnum(methodDetailsInfo.NodeType, out var nodeType)) - { - await flowEnvironment.CreateNodeAsync(nodeType, position, methodDetailsInfo); - } - }); - } + #endregion + #region 转发事件的处理 /// /// 从工作台事件转发器监听节点创建事件 @@ -225,6 +219,178 @@ namespace Serein.Workbench.Avalonia.Services } + + /// + /// 运行环境连接了节点事件 + /// + /// + /// + private void FeefService_OnNodeConnectChange(NodeConnectChangeEventArgs eventArgs) + { +#if false + string fromNodeGuid = eventArgs.FromNodeGuid; + string toNodeGuid = eventArgs.ToNodeGuid; + if (!TryGetControl(fromNodeGuid, out var fromNodeControl) + || !TryGetControl(toNodeGuid, out var toNodeControl)) + { + return; + } + + if (eventArgs.JunctionOfConnectionType == JunctionOfConnectionType.Invoke) + { + ConnectionInvokeType connectionType = eventArgs.ConnectionInvokeType; + #region 创建/删除节点之间的调用关系 + #region 创建连接 + if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 + { + if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) + { + SereinEnv.WriteLine(InfoType.INFO, "非预期的连接"); + return; + } + var startJunction = IFormJunction.NextStepJunction; + var endJunction = IToJunction.ExecuteJunction; + + startJunction.TransformToVisual(MainCanvas); + + // 添加连接 + var shape = new ConnectionLineShape( + FlowChartCanvas, + connectionType, + startJunction, + endJunction + ); + NodeConnectionLine nodeConnectionLine = new NodeConnectionLine(MainCanvas, shape); + + //if (toNodeControl is FlipflopNodeControl flipflopControl + // && flipflopControl?.ViewModel?.NodeModel is NodeModelBase nodeModel) // 某个节点连接到了触发器,尝试从全局触发器视图中移除该触发器 + //{ + // NodeTreeViewer.RemoveGlobalFlipFlop(nodeModel); // 从全局触发器树树视图中移除 + //} + + Connections.Add(nodeConnectionLine); + fromNodeControl.AddCnnection(shape); + toNodeControl.AddCnnection(shape); + } + #endregion +#if false + + #region 移除连接 + else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 + { + // 需要移除连接 + var removeConnections = Connections.Where(c => + c.Start.MyNode.Guid.Equals(fromNodeGuid) + && c.End.MyNode.Guid.Equals(toNodeGuid) + && (c.Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke + || c.End.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke)) + .ToList(); + + + foreach (var connection in removeConnections) + { + Connections.Remove(connection); + fromNodeControl.RemoveConnection(connection); // 移除连接 + toNodeControl.RemoveConnection(connection); // 移除连接 + if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) + { + JudgmentFlipFlopNode(control); // 连接关系变更时判断 + } + } + } + #endregion + +#endif + #endregion + } + else + { + #if false + ConnectionArgSourceType connectionArgSourceType = eventArgs.ConnectionArgSourceType; + #region 创建/删除节点之间的参数传递关系 + #region 创建连接 + if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 + { + if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) + { + SereinEnv.WriteLine(InfoType.INFO, "非预期的情况"); + return; + } + + JunctionControlBase startJunction = eventArgs.ConnectionArgSourceType switch + { + ConnectionArgSourceType.GetPreviousNodeData => IFormJunction.ReturnDataJunction, // 自身节点 + ConnectionArgSourceType.GetOtherNodeData => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 + ConnectionArgSourceType.GetOtherNodeDataOfInvoke => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 + _ => throw new Exception("窗体事件 FlowEnvironment_NodeConnectChangeEvemt 创建/删除节点之间的参数传递关系 JunctionControlBase 枚举值错误 。非预期的枚举值。") // 应该不会触发 + }; + + if (IToJunction.ArgDataJunction.Length <= eventArgs.ArgIndex) + { + _ = Task.Run(async () => + { + await Task.Delay(500); + FlowEnvironment_NodeConnectChangeEvemt(eventArgs); + }); + return; + } + JunctionControlBase endJunction = IToJunction.ArgDataJunction[eventArgs.ArgIndex]; + LineType lineType = LineType.Bezier; + // 添加连接 + var connection = new ConnectionControl( + lineType, + FlowChartCanvas, + eventArgs.ArgIndex, + eventArgs.ConnectionArgSourceType, + startJunction, + endJunction, + IToJunction + ); + Connections.Add(connection); + fromNodeControl.AddCnnection(connection); + toNodeControl.AddCnnection(connection); + EndConnection(); // 环境触发了创建节点连接事件 + + + } + #endregion + #region 移除连接 + else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 + { + // 需要移除连接 + var removeConnections = Connections.Where(c => c.Start.MyNode.Guid.Equals(fromNodeGuid) + && c.End.MyNode.Guid.Equals(toNodeGuid)) + .ToList(); // 获取这两个节点之间的所有连接关系 + + + + foreach (var connection in removeConnections) + { + if (connection.End is ArgJunctionControl junctionControl && junctionControl.ArgIndex == eventArgs.ArgIndex) + { + // 找到符合删除条件的连接线 + Connections.Remove(connection); // 从本地记录中移除 + fromNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 + toNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 + } + + + //if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) + //{ + // JudgmentFlipFlopNode(control); // 连接关系变更时判断 + //} + } + } + #endregion + #endregion +#endif + } +#endif + } + #endregion + + #region 私有方法 + /// /// 创建节点控件 /// @@ -234,7 +400,7 @@ namespace Serein.Workbench.Avalonia.Services /// 返回的节点对象 /// 是否创建成功 /// 无法创建节点控件 - private bool TryCreateNodeView(Type viewType, Type viewModelType, NodeModelBase nodeModel, out INodeControl? nodeView) + private bool TryCreateNodeView(Type viewType, Type viewModelType, NodeModelBase nodeModel, out NodeControlBase? nodeView) { if (string.IsNullOrEmpty(nodeModel.Guid)) { @@ -248,16 +414,16 @@ namespace Serein.Workbench.Avalonia.Services } viewModelBase.NodeModelBase = nodeModel; // 设置节点对象 var controlObj = Activator.CreateInstance(viewType); - if (controlObj is not INodeControl nodeControl) + if (controlObj is NodeControlBase nodeControl) { - nodeView = null; - return false; + nodeControl.DataContext = viewModelBase; + nodeView = nodeControl; + return true; } else { - nodeControl.SetNodeModel(nodeModel); - nodeView = nodeControl; - return true; + nodeView = null; + return false; } // 在其它地方验证过了,所以注释 @@ -276,6 +442,47 @@ namespace Serein.Workbench.Avalonia.Services //} } + private bool TryGetControl(string nodeGuid, out NodeControlBase nodeControl) + { + if (string.IsNullOrEmpty(nodeGuid)) + { + nodeControl = null; + return false; + } + if (!NodeControls.TryGetValue(nodeGuid, out nodeControl)) + { + nodeControl = null; + return false; + } + if (nodeControl is null) + { + return false; + } + return true; + } + + #endregion + + #region 操作接口对外暴露的接口 + + /// + /// 创建节点控件 + /// + /// 控件类型 + /// 创建坐标 + /// 节点方法信息(基础节点传null) + public void CreateNodeView(MethodDetailsInfo methodDetailsInfo, PositionOfUI position) + { + Task.Run(async () => + { + if (EnumHelper.TryConvertEnum(methodDetailsInfo.NodeType, out var nodeType)) + { + await flowEnvironment.CreateNodeAsync(nodeType, position, methodDetailsInfo); + } + }); + } + + /// /// 尝试在连接控制点之间创建连接线 /// @@ -283,25 +490,23 @@ namespace Serein.Workbench.Avalonia.Services { if (MainCanvas is not null) { - var myData = ConnectingData; - var junctionSize = startJunction.GetTransformedBounds()!.Value.Bounds.Size; - var junctionPoint = new Point(junctionSize.Width / 2, junctionSize.Height / 2); - if (startJunction.TranslatePoint(junctionPoint, MainCanvas) is Point point) + ConnectingData.Reset(); + ConnectingData.IsCreateing = true; // 表示开始连接 + ConnectingData.StartJunction = startJunction; + ConnectingData.CurrentJunction = startJunction; + if(startJunction.JunctionType == JunctionType.NextStep || startJunction.JunctionType == JunctionType.ReturnData) { - myData.StartPoint = point; + + ConnectingData.TempLine = new NodeConnectionLineView(MainCanvas, startJunction, null); } else { - return; + ConnectingData.TempLine = new NodeConnectionLineView(MainCanvas,null ,startJunction); } - myData.Reset(); - myData.IsCreateing = true; // 表示开始连接 - myData.StartJunction = startJunction; - myData.CurrentJunction = startJunction; - var junctionOfConnectionType = startJunction.JunctionType.ToConnectyionType(); - ConnectionLineShape bezierLine; // 类别 + /*var junctionOfConnectionType = startJunction.JunctionType.ToConnectyionType(); + ConnectionLineShape bezierLine; Brush brushColor; // 临时线的颜色 if (junctionOfConnectionType == JunctionOfConnectionType.Invoke) { @@ -319,11 +524,13 @@ namespace Serein.Workbench.Avalonia.Services myData.StartPoint, brushColor, isTop: true); // 绘制临时的线 - + */ //Mouse.OverrideCursor = Cursors.Cross; // 设置鼠标为正在创建连线 - myData.TempLine = new MyLine(MainCanvas, bezierLine); + } - } + } + + #endregion } } diff --git a/Serein.Workbench.Avalonia/Views/MainView.axaml b/Serein.Workbench.Avalonia/Views/MainView.axaml index a26f1c7..fee1b0a 100644 --- a/Serein.Workbench.Avalonia/Views/MainView.axaml +++ b/Serein.Workbench.Avalonia/Views/MainView.axaml @@ -18,7 +18,7 @@ - + diff --git a/WorkBench/Node/View/ActionNodeControl.xaml.cs b/WorkBench/Node/View/ActionNodeControl.xaml.cs index f26e7ba..a35d624 100644 --- a/WorkBench/Node/View/ActionNodeControl.xaml.cs +++ b/WorkBench/Node/View/ActionNodeControl.xaml.cs @@ -18,8 +18,7 @@ namespace Serein.Workbench.Node.View InitializeComponent(); if(ExecuteJunctionControl.MyNode != null) { - - ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid; + ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid; } }