MainForm.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. using Microsoft.Extensions.Logging.Console;
  2. using MySql.Data.MySqlClient;
  3. using SinMaiLauncher.ChildProcessHolder;
  4. using SinMaiLauncher.Interops;
  5. using SinMaiLauncher.Models;
  6. namespace SinMaiLauncher;
  7. public partial class MainForm : Form
  8. {
  9. private ILogger logger;
  10. private SettingReader setting;
  11. private Dictionary<ChildProcessKind, ChildProcessControlGroup> childProcessControlGroups;
  12. private readonly IEnumerator<string> txtAni = TextAnimation.LoadingTextSeq(8).GetEnumerator();
  13. private string mainControlStatusText = "摸鱼中";
  14. private bool mainControlStatusTextAnimation = false;
  15. private DateTime lastCheckSinMaiMainWinPos = DateTime.Now;
  16. private DateTime lastCheckSysVolume = DateTime.Now;
  17. private List<string> holdingExplorerWindowPaths = new();
  18. public MainForm()
  19. {
  20. InitializeComponent();
  21. }
  22. private WindowInterops.MoveWindowResult MoveSinMaiMainWindow()
  23. {
  24. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  25. //WS_CLIPSIBLINGS WS_POPUPWINDOW WS_VISIBLE
  26. var rc = setting.Settings.AutoRect;
  27. return WindowInterops.MoveWindow(pb.HWndMainWindow.Value, rc.Left, rc.Top, rc.Width, rc.Height);
  28. }
  29. private void ExplorerSaveAndKill()
  30. {
  31. holdingExplorerWindowPaths.Clear();
  32. var lst = ExplorerInterops.GetOpenWindows();
  33. logger.LogInformation($"explorer saved paths:{lst.Length}");
  34. holdingExplorerWindowPaths.AddRange(lst);
  35. ExplorerInterops.KillExplorer();
  36. }
  37. private async Task ExplorerRestore()
  38. {
  39. ExplorerInterops.RestoreExplorer();
  40. await Task.Delay(1000);
  41. ExplorerInterops.OpenWindow(holdingExplorerWindowPaths);
  42. }
  43. private async void MainForm_Shown(object sender, EventArgs e)
  44. {
  45. TopMost = false;
  46. TopMost = true;
  47. tcMain.SelectedTab = tpConsoles;
  48. ConsoleInterops.CurrentProcess.ConsoleAllocate();
  49. ConsoleInterops.CurrentProcess.ConsoleDisableQuickEdit();
  50. ConsoleInterops.CurrentProcess.ConsoleWindowSetParent(tpConsoleLauncher);
  51. ConsoleInterops.CurrentProcess.ConsoleWindowFill(tpConsoleLauncher);
  52. tpConsoleLauncher.Resize += delegate { ConsoleInterops.CurrentProcess.ConsoleWindowFill(tpConsoleLauncher); };
  53. //start host application
  54. var builder = Host.CreateDefaultBuilder();
  55. builder.ConfigureServices((ctx, services) =>
  56. {
  57. //控制台日志格式
  58. services.AddLogging(opt =>
  59. {
  60. opt.AddSimpleConsole(p =>
  61. {
  62. p.TimestampFormat = "[dd HH:mm:ss] ";
  63. p.SingleLine = true;
  64. p.ColorBehavior = LoggerColorBehavior.Enabled;
  65. })
  66. .AddDebug()
  67. //.AddProvider(loggingIntoEventBus)
  68. ;
  69. services.AddSingleton(opt);
  70. });
  71. services.AddSingleton(this);
  72. });
  73. var host = builder.Build();
  74. var services = host.Services;
  75. host.Start();
  76. logger = services.GetRequiredService<ILogger<MainForm>>();
  77. ExplorerInterops.logger = logger;
  78. var conf = services.GetRequiredService<IConfiguration>();
  79. var settingModel = new SinMaiLauncherSettingModel();
  80. conf.GetSection("SinMaiLauncher").Bind(settingModel);
  81. setting = new(settingModel);
  82. childProcessControlGroups = new Dictionary<ChildProcessKind, ChildProcessControlGroup>
  83. {
  84. {
  85. ChildProcessKind.MariaDb, new ChildProcessControlGroup
  86. {
  87. StateBag = new ProcessStateBagMariaDb(services.GetRequiredService<ILogger<ProcessStateBagMariaDb>>(), setting),
  88. ConsoleTab = tpConsoleMaria, PidLabel = lblPidMaria, StatusLabel = lblSubControlMaria,
  89. StartButton = btnSubControlMariaStart, StopButton = btnSubControlMariaStop
  90. }
  91. },
  92. {
  93. ChildProcessKind.AquaDx, new ChildProcessControlGroup
  94. {
  95. StateBag = new ProcessStateBagAquaDx(services.GetRequiredService<ILogger<ProcessStateBagAquaDx>>(), setting),
  96. Dep = ChildProcessKind.MariaDb,
  97. ConsoleTab = tpConsoleAquaDx, PidLabel = lblPidAquaDx, StatusLabel = lblSubControlAquaDx,
  98. StartButton = btnSubControlAquaDxStart, StopButton = btnSubControlAquaDxStop
  99. }
  100. },
  101. {
  102. ChildProcessKind.Injector, new ChildProcessControlGroup
  103. {
  104. StateBag = new ChildProcessStateBagForConsole(ChildProcessKind.Injector, setting.Injector),
  105. Dep = ChildProcessKind.AquaDx,
  106. ConsoleTab = tpConsoleInjector, PidLabel = lblPidInjector, StatusLabel = lblSubControlInjector,
  107. StartButton = btnSubControlInjectorStart, StopButton = btnSubControlInjectorStop
  108. }
  109. },
  110. {
  111. ChildProcessKind.SinMai, new ChildProcessControlGroup
  112. {
  113. StateBag = new ChildProcessStateBagForWin(ChildProcessKind.SinMai, setting.SinMai),
  114. Dep = ChildProcessKind.Injector,
  115. ConsoleTab = null, PidLabel = lblPidSinMai, StatusLabel = lblSubControlSinMai,
  116. StartButton = btnSubControlSinMaiStart, StopButton = btnSubControlSinMaiStop
  117. }
  118. }
  119. };
  120. foreach (var kvp in childProcessControlGroups)
  121. {
  122. var kind = kvp.Key;
  123. var gr = kvp.Value;
  124. var tab = gr.ConsoleTab;
  125. var cpb = gr.StateBag;
  126. var btnStart = gr.StartButton;
  127. var btnStop = gr.StopButton;
  128. var lblPid = gr.PidLabel;
  129. var lblStatus = gr.StatusLabel;
  130. //绑定tab控制台窗口填充
  131. if (cpb is ChildProcessStateBagForConsole con)
  132. {
  133. tab.Resize += delegate
  134. {
  135. if (con.HWndConsole.HasValue) WindowInterops.FillWindow(con.HWndConsole.Value, tab);
  136. };
  137. }
  138. btnStart.Click += async delegate
  139. {
  140. if (cpb.IsAlive)
  141. {
  142. MessageBox.Show("它在运行了,别重复点启动啊");
  143. return;
  144. }
  145. if (gr.Dep.HasValue && childProcessControlGroups[gr.Dep.Value].StateBag.Status != ChildProcessStatus.Ready)
  146. {
  147. MessageBox.Show("依赖的进程未就绪,启动不了的,等就绪了再启动。要按顺序启动。");
  148. return;
  149. }
  150. await cpb.StartAsync();
  151. };
  152. btnStop.Click += async delegate
  153. {
  154. if (cpb.IsAlive == false)
  155. {
  156. MessageBox.Show("都没启动,咋停止啊?");
  157. return;
  158. }
  159. await cpb.StopAsync(TimeSpan.FromSeconds(5));
  160. };
  161. cpb.StatusUpdated += async delegate
  162. {
  163. void UpdateUi()
  164. {
  165. lblPid.Text = cpb.Pid.HasValue ? cpb.Pid.Value.ToString() : "-";
  166. lblStatus.Text = cpb.Status switch
  167. {
  168. ChildProcessStatus.NoLaunched => "未启动",
  169. ChildProcessStatus.PreLaunching => "准备启动",
  170. ChildProcessStatus.MissingWorkingDir => "丢失工作目录",
  171. ChildProcessStatus.MissingExeFile => "丢失exe文件",
  172. ChildProcessStatus.Launching => "正在启动",
  173. ChildProcessStatus.WaitingReady => "等待就绪...",
  174. ChildProcessStatus.Ready => "就绪",
  175. ChildProcessStatus.Stopping => "正在停止",
  176. ChildProcessStatus.Stopped => "已停止",
  177. _ => "?"
  178. };
  179. if (cpb.Status == ChildProcessStatus.WaitingReady && cpb is ChildProcessStateBagForConsole con && cpb.IsAlive && cpb.Pid.HasValue && con.HWndConsole.HasValue)
  180. {
  181. if (tab != null)
  182. {
  183. //console 收纳
  184. Program.DisableQuickEdit(cpb.Pid.Value);
  185. WindowInterops.RemoveBorderVisible(con.HWndConsole.Value);
  186. WindowInterops.SetParent(con.HWndConsole.Value, tab.Handle);
  187. WindowInterops.FillWindow(con.HWndConsole.Value, tab);
  188. Height++;
  189. Height--;
  190. }
  191. }
  192. if (chkExplorerAuto.Checked)
  193. {
  194. if (kind == ChildProcessKind.SinMai)
  195. {
  196. if (cpb.Status == ChildProcessStatus.Ready)
  197. {
  198. ExplorerSaveAndKill();
  199. }
  200. if (cpb.Status == ChildProcessStatus.Stopped)
  201. {
  202. logger.LogInformation("SinMai Exit");
  203. _ = ExplorerRestore();
  204. }
  205. }
  206. }
  207. }
  208. if (InvokeRequired) await InvokeAsync(UpdateUi);
  209. else UpdateUi();
  210. };
  211. }
  212. logger.LogInformation("Started");
  213. Application.DoEvents();
  214. await Task.Delay(1000);
  215. tcMain.Enabled = true;
  216. //#if DEBUG
  217. // tcMain.SelectedTab = tpSubControls;
  218. //#else
  219. tcMain.SelectedTab = tpMainControl;
  220. //#endif
  221. }
  222. private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
  223. {
  224. if (
  225. childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.Status == ChildProcessStatus.Ready
  226. || childProcessControlGroups[ChildProcessKind.AquaDx].StateBag.Status == ChildProcessStatus.Ready
  227. )
  228. {
  229. e.Cancel = MessageBox.Show($"基础设施进程仍在运行。{Environment.NewLine}为了避免进度数据裂开,请确保安全停止再关闭。{Environment.NewLine}要强制关闭吗?", "关闭启动器", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.No;
  230. }
  231. }
  232. private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
  233. {
  234. //Kill all child process
  235. childProcessControlGroups[ChildProcessKind.SinMai].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  236. childProcessControlGroups[ChildProcessKind.Injector].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  237. childProcessControlGroups[ChildProcessKind.AquaDx].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  238. childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.StopAsync(TimeSpan.FromSeconds(10)).Wait(TimeSpan.FromSeconds(10));
  239. }
  240. private void tcConsoles_SelectedIndexChanged(object sender, EventArgs e)
  241. {
  242. Height++;
  243. Height--;
  244. }
  245. private async void btnMainControlStart_Click(object sender, EventArgs e)
  246. {
  247. btnMainControlStart.Enabled = false;
  248. mainControlStatusTextAnimation = true;
  249. var orderedProcesses = new List<(ChildProcessKind kind, TimeSpan timeout)>
  250. {
  251. (ChildProcessKind.MariaDb, TimeSpan.FromSeconds(30)),
  252. (ChildProcessKind.AquaDx,TimeSpan.FromSeconds(90)),
  253. (ChildProcessKind.Injector,TimeSpan.FromSeconds(30)),
  254. (ChildProcessKind.SinMai,TimeSpan.FromSeconds(30)),
  255. };
  256. var startedProcesses = new List<ChildProcessKind>();
  257. try
  258. {
  259. foreach (var (kind, timeout) in orderedProcesses)
  260. {
  261. var gr = childProcessControlGroups[kind];
  262. var cpb = gr.StateBag;
  263. mainControlStatusText = $"正在启动 {kind}";
  264. if (cpb.IsAlive)
  265. {
  266. logger.LogWarning("{Kind} is already running, skipping.", kind);
  267. startedProcesses.Add(kind); // 已运行的也记录,便于回滚
  268. continue;
  269. }
  270. await cpb.StartAsync();
  271. await WaitForReadyAsync(cpb, timeout);
  272. if (cpb.Status != ChildProcessStatus.Ready)
  273. {
  274. logger.LogError($"{kind} failed to reach Ready state (current: {cpb.Status}), rolling back.");
  275. await RollbackAsync(startedProcesses);
  276. return;
  277. }
  278. startedProcesses.Add(kind);
  279. }
  280. mainControlStatusText = $"已启动!";
  281. btnMainControlStop.Enabled = true;
  282. mainControlStatusTextAnimation = false;
  283. }
  284. catch (Exception exception)
  285. {
  286. logger.LogError(exception, "Error during startup, rolling back.");
  287. await RollbackAsync(startedProcesses);
  288. }
  289. async Task WaitForReadyAsync(ChildProcessStateBag cpb, TimeSpan timeout)
  290. {
  291. if (cpb.Status == ChildProcessStatus.Ready
  292. || cpb.Status == ChildProcessStatus.MissingWorkingDir
  293. || cpb.Status == ChildProcessStatus.MissingExeFile
  294. || cpb.Status == ChildProcessStatus.Stopped) return;
  295. var tcs = new TaskCompletionSource<bool>();
  296. EventHandler<EventArgs> handler = null;
  297. handler = delegate
  298. {
  299. if (cpb.Status == ChildProcessStatus.Ready
  300. || cpb.Status == ChildProcessStatus.MissingWorkingDir
  301. || cpb.Status == ChildProcessStatus.MissingExeFile
  302. || cpb.Status == ChildProcessStatus.Stopped
  303. )
  304. {
  305. cpb.StatusUpdated -= handler;
  306. tcs.TrySetResult(true);
  307. }
  308. };
  309. cpb.StatusUpdated += handler;
  310. try
  311. {
  312. // 等待 Ready 或超时
  313. await Task.WhenAny(tcs.Task, Task.Delay(timeout));
  314. if (!tcs.Task.IsCompleted)
  315. {
  316. cpb.StatusUpdated -= handler;
  317. throw new TimeoutException($"{cpb.Kind} did not reach Ready state within {timeout.TotalSeconds} seconds.");
  318. }
  319. }
  320. finally
  321. {
  322. cpb.StatusUpdated -= handler; // 确保清理
  323. }
  324. }
  325. async Task RollbackAsync(List<ChildProcessKind> startedProcesses)
  326. {
  327. // 按相反顺序停止
  328. foreach (var kind in startedProcesses.AsEnumerable().Reverse())
  329. {
  330. mainControlStatusText = $"正在停止 {kind}";
  331. var group = childProcessControlGroups[kind];
  332. var cpb = group.StateBag;
  333. if (cpb.IsAlive)
  334. {
  335. logger.LogInformation("Stopping {Kind} during rollback...", kind);
  336. await InvokeAsync(() => group.StatusLabel.Text = "正在停止...");
  337. await cpb.StopAsync(TimeSpan.FromSeconds(5));
  338. }
  339. }
  340. mainControlStatusText = "启动失败,已回滚到启动之前";
  341. btnMainControlStart.Enabled = true;
  342. mainControlStatusTextAnimation = false;
  343. }
  344. }
  345. private async void btnMainControlStop_Click(object sender, EventArgs e)
  346. {
  347. var orderedProcesses = new List<(ChildProcessKind kind, TimeSpan timeout)>
  348. {
  349. (ChildProcessKind.SinMai,TimeSpan.FromSeconds(5)),
  350. (ChildProcessKind.Injector,TimeSpan.FromSeconds(10)),
  351. (ChildProcessKind.AquaDx,TimeSpan.FromSeconds(90)),
  352. (ChildProcessKind.MariaDb, TimeSpan.FromSeconds(30)),
  353. };
  354. foreach (var (kind, timeout) in orderedProcesses)
  355. {
  356. mainControlStatusText = $"正在停止 {kind}";
  357. var gr = childProcessControlGroups[kind];
  358. if (gr.StateBag.IsAlive)
  359. {
  360. await gr.StateBag.StopAsync(timeout);
  361. }
  362. }
  363. btnMainControlStart.Enabled = true;
  364. btnMainControlStop.Enabled = false;
  365. mainControlStatusText = $"已停止";
  366. mainControlStatusTextAnimation = false;
  367. }
  368. private void tmrMain_Tick(object sender, EventArgs e)
  369. {
  370. if (mainControlStatusTextAnimation)
  371. {
  372. txtAni.MoveNext();
  373. lblMainControlStatus.Text = $"{mainControlStatusText} {txtAni.Current}";
  374. }
  375. else
  376. {
  377. lblMainControlStatus.Text = $"{mainControlStatusText}";
  378. }
  379. //窗口调整
  380. if (chkWinTrickAuto.Checked && lastCheckSinMaiMainWinPos < DateTime.Now.AddSeconds(-5))
  381. {
  382. lastCheckSinMaiMainWinPos = DateTime.Now;
  383. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  384. if (pb.Status == ChildProcessStatus.Ready && pb.HWndMainWindow.HasValue)
  385. {
  386. MoveSinMaiMainWindow();
  387. }
  388. }
  389. //显示系统音量
  390. if (lastCheckSysVolume < DateTime.Now.AddSeconds(-1))
  391. {
  392. lastCheckSinMaiMainWinPos = DateTime.Now;
  393. var sv = SystemAudioVolumeInterop.GetVolume();
  394. lblSysVolume.Text = sv.HasValue ? $"{sv:N2}%" : "-";
  395. }
  396. }
  397. private void btnAimeWrite_Click(object sender, EventArgs e)
  398. {
  399. var input = cboAimeCard.Text;
  400. if (string.IsNullOrEmpty(input))
  401. {
  402. MessageBox.Show("倒是输入aime卡号啊?");
  403. return;
  404. }
  405. if (Directory.Exists(setting.AimeFleSinMaiDir) == false)
  406. {
  407. MessageBox.Show("找不到写aime卡文件的目录");
  408. return;
  409. }
  410. if (Directory.Exists(setting.AimeFleSinMaiPath))
  411. {
  412. MessageBox.Show("找不到写aime卡的文件");
  413. return;
  414. }
  415. var aime = input.Replace("-", "");
  416. File.WriteAllText(setting.AimeFleSinMaiPath, aime);
  417. MessageBox.Show($"已将选定的卡号已写入 aime.txt !{Environment.NewLine}在游戏登录时按回车键读卡{Environment.NewLine}(按一次回车不行就多按几次回车键直到读卡成功🙄)");
  418. }
  419. private void btnWinTrick_Click(object sender, EventArgs e)
  420. {
  421. var pb = (ChildProcessStateBagForWin)childProcessControlGroups[ChildProcessKind.SinMai].StateBag;
  422. if (pb.Status != ChildProcessStatus.Ready || !pb.HWndMainWindow.HasValue)
  423. {
  424. MessageBox.Show("游戏启动之后再点这个按钮");
  425. return;
  426. }
  427. var moveWindowResult = MoveSinMaiMainWindow();
  428. switch (moveWindowResult)
  429. {
  430. case WindowInterops.MoveWindowResult.FailGet:
  431. MessageBox.Show("操作不成功:读取失败");
  432. break;
  433. case WindowInterops.MoveWindowResult.FailSet:
  434. MessageBox.Show("操作不成功:设置失败");
  435. break;
  436. case WindowInterops.MoveWindowResult.Success:
  437. MessageBox.Show("操作成功");
  438. break;
  439. case WindowInterops.MoveWindowResult.NoChange:
  440. MessageBox.Show("没有改变");
  441. break;
  442. }
  443. }
  444. private void btnAimeRefresh_Click(object sender, EventArgs e)
  445. {
  446. if (childProcessControlGroups[ChildProcessKind.MariaDb].StateBag.Status != ChildProcessStatus.Ready)
  447. {
  448. MessageBox.Show("启动数据库先啊!");
  449. return;
  450. }
  451. var conf = setting.Settings.Infra.MariaDb;
  452. var connectionString = $"Server={conf.Host};Port={conf.Port};Uid={conf.Usr};Pwd={conf.Pwd};";
  453. using var conn = new MySqlConnection(connectionString);
  454. conn.Open();
  455. using var cmd = conn.CreateCommand();
  456. cmd.CommandText = "SELECT `luid` FROM `aqua`.`sega_card`";
  457. using var dr = cmd.ExecuteReader();
  458. cboAimeCard.Items.Clear();
  459. while (dr.Read())
  460. {
  461. var aimeR = dr["luid"].ToString();
  462. var aimeH = string.Join("-",
  463. aimeR.Substring(0, 4),
  464. aimeR.Substring(4, 4),
  465. aimeR.Substring(8, 4),
  466. aimeR.Substring(12, 4),
  467. aimeR.Substring(16, 4));
  468. cboAimeCard.Items.Add(aimeH);
  469. }
  470. MessageBox.Show($"从数据库中读取到 {cboAimeCard.Items.Count} 个卡号已载入下拉列表");
  471. }
  472. private void btnExplorerKill_Click(object sender, EventArgs e)
  473. {
  474. ExplorerSaveAndKill();
  475. }
  476. private async void btnExplorerRestore_Click(object sender, EventArgs e)
  477. {
  478. await ExplorerRestore();
  479. }
  480. private void btnProcessLoopBackVirtualNic_Click(object sender, EventArgs e)
  481. {
  482. MessageBox.Show("还没做!");
  483. }
  484. private void btnAppDataOverwrite_Click(object sender, EventArgs e)
  485. {
  486. MessageBox.Show("还没做!");
  487. }
  488. private void btnSysVolumeDec_Click(object sender, EventArgs e)
  489. {
  490. var sv = SystemAudioVolumeInterop.GetVolume();
  491. if (!sv.HasValue)
  492. {
  493. MessageBox.Show("无法获取当前音量");
  494. return;
  495. }
  496. sv -= 5;
  497. if (sv < 0) sv = 0;
  498. if (SystemAudioVolumeInterop.SetVolume(sv.Value) == false) MessageBox.Show("音量调整失败");
  499. }
  500. private void btnSysVolumeInc_Click(object sender, EventArgs e)
  501. {
  502. var sv = SystemAudioVolumeInterop.GetVolume();
  503. if (!sv.HasValue)
  504. {
  505. MessageBox.Show("无法获取当前音量");
  506. return;
  507. }
  508. sv += 5;
  509. if (sv > 100) sv = 100;
  510. if (SystemAudioVolumeInterop.SetVolume(sv.Value) == false) MessageBox.Show("音量调整失败");
  511. }
  512. private class ChildProcessControlGroup
  513. {
  514. public ChildProcessStateBag StateBag { get; set; }
  515. public ChildProcessKind? Dep { get; set; }
  516. public TabPage? ConsoleTab { get; set; } // 可空,窗口进程为 null
  517. public Label PidLabel { get; set; }
  518. public Label StatusLabel { get; set; }
  519. public Button StartButton { get; set; }
  520. public Button StopButton { get; set; }
  521. }
  522. }