作为程序员,我们的生产皆来源于需求,本次的需求是把.Net core WebAPI托管进窗体程序,缘由是.net core的控制台窗口很容易被人误关,常见的办法是把程序做成windows服务,开机自启,不过windows服务是完全隐藏的,如果用户想在任务栏中看到进程,想在退出时输入密码确认,我暂时能想到的就是用窗体程序接管WebAPI程序,需求不是非常好,但也合情合理。
废话不多说,进入正题。
首先,我这里用WPF来实现(Winform也是基本一样的),计划是在窗体程序中,不仅接管WebAPI程序,同时把控制台的内容输出到WPF的富文本中,关闭窗体程序后,最小化到系统托盘,右键程序图标,可恢复窗口、可退出进程。
vs中创建WPF程序后,修改主窗体及后置代码:
<Window x:Class="NETCore.ApiHosted.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NETCore.ApiHosted"
mc:Ignorable="d"
Title="NETCore.ApiHosted" Height="450" Width="800">
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<RichTextBox Name="rtbOutput" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</ScrollViewer>
</Grid>
</Window>
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private NotifyIcon notifyIcon;
private ContextMenuStrip contextMenuStrip;
private Process webApiProcess;
public MainWindow()
{
this.InitializeComponent();
// 将WebAPI控制台输出重定向到WPF窗口
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c dotnet HostedConsole.dll",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
webApiProcess = new Process
{
StartInfo = startInfo
};
webApiProcess.OutputDataReceived += (sender, e) =>
{
Dispatcher.Invoke(() =>
{
// 将WebAPI的输出写入到WPF窗口的控件中,如一个TextBox或RichTextBox
rtbOutput.AppendText(e.Data + Environment.NewLine);
});
};
webApiProcess.Start();
webApiProcess.BeginOutputReadLine();
// 创建托盘图标
notifyIcon = new NotifyIcon
{
Icon = new System.Drawing.Icon("ApiHosted.ico"),
Visible = true,
Text = "WebAPI 托管程序"
};
notifyIcon.DoubleClick += (sender, args) => ShowMain();
contextMenuStrip = new ContextMenuStrip();
contextMenuStrip.Items.Add("Restore", null, (s, e) => ShowMain());
contextMenuStrip.Items.Add("Exit", null, (s, e) => ExitMenuItem_Click(s, e));
notifyIcon.ContextMenuStrip = contextMenuStrip;
// 窗口关闭时最小化到托盘
this.Closing += (s, e) =>
{
e.Cancel = true;
this.Hide();
};
}
private void ExitMenuItem_Click(object sender, EventArgs e)
{
var dialog = new PasswordWindow();
dialog.btnExit.Click += (s, e) =>
{
string username = dialog.UserName.Text;
string password = dialog.PwBox.Password;
// 在这里验证用户名和密码的正确性
if (username == "admin" && password == "123456")
{
// 输入用户名和密码正确后退出应用程序
webApiProcess.Kill();
notifyIcon.Dispose();
System.Windows.Application.Current.Shutdown();
}
else
{
MessageBox.Show("用户名或密码错误,请重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
};
dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
dialog.ShowDialog();
}
private void ShowMain()
{
this.Show();
WindowState = WindowState.Normal;
}
}
由于NotifyIcon类和ContextMenuStrip类来自于winform的dll,因此还需要在程序配置信息中加一段代码,否则无法通过编译

创建测试程序HostedConsole:控制台应用程序,代码很简单,不断的输出信息即可
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
while (true)
{
Console.WriteLine($"{DateTime.Now} Hello. {new Random().Next(0, 10000)}");
Thread.Sleep(1000);
}
然后,编译两个程序。
把HostedConsole的编译后的exe和dll,copy到NETCore.ApiHosted的bin目录下,总之,让两个程序的编译包都放在一起,当然也可以创建文件夹进行管理,更好看一些。
最后,启动NETCore.ApiHosted.exe进程,可以看到NETCore.ApiHosted启动的时候,也启动了HostedConsole,并且把HostedConsole的控制台输出项取了过来,输出在富文本中。

关闭窗口,最小化到托盘
右键进程 
是想要的效果,OK,Restore恢复窗体,Exit退出进程,需要出确认框,输入账号密码退出。
密码确认窗体:
<Window x:Class="NETCore.ApiHosted.PasswordWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NETCore.ApiHosted"
mc:Ignorable="d"
Title="Exit Confirm" Height="200" Width="300">
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="用户名" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right"></TextBlock>
<TextBox x:Name="UserName" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left" TextWrapping="Wrap" Margin="10" VerticalAlignment="Center" Width="150"/>
<TextBlock Text="密码" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right"></TextBlock>
<PasswordBox x:Name="PwBox" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Width="150" Margin="10" />
<Button x:Name="btnExit" Content="Exit" Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Center" Margin="10"
VerticalAlignment="Top" Width="100" Height="30" IsDefault="True">
</Button>
</Grid>
</Window>
不知你有没有发现?退出进程的代码存在Bug,因为只是退出了NETCore.ApiHosted,却没有退出HostedConsole,所以需要进一步提升代码质量。
我的处理方式是,自己的webapi程序有一个固定端口,
Process[] dotnetProcesses = Process.GetProcessesByName("dotnet");
使用这段的代码找到所有dotnet进程,遍历进程,找到该端口的进程,Kill it!
贴出代码:
private void ExitMenuItem_Click(object sender, EventArgs e)
{
var dialog = new PasswordWindow();
dialog.btnExit.Click += (s, e) =>
{
string username = dialog.UserName.Text;
string password = dialog.PwBox.Password;
// 在这里验证用户名和密码的正确性
if (username == "admin" && password == "123456")
{
string processName = "dotnet";
int port = 你的webapi端口;
// 获取所有名为dotnet的进程
Process[] dotnetProcesses = Process.GetProcessesByName(processName);
foreach (Process process in dotnetProcesses)
{
try
{
// 尝试获取进程打开的所有端口
foreach (var connection in IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners())
{
// 检查端口和进程ID是否匹配
if (connection.Port != port) continue;
// 由于我们无法直接从connection获取进程ID,我们需要检查dotnet进程是否有可能监听该端口
// 一种简单的方法是检查进程启动的命令行参数是否包含端口号
if (process.MainModule.FileName.EndsWith("dotnet.exe", StringComparison.OrdinalIgnoreCase))
{
if (process.ProcessName == "dotnet")
{
Console.WriteLine($"找到监听端口 {port} 的 dotnet 进程: {process.ProcessName} (ID: {process.Id})");
process.Kill(); // 关闭进程
Console.WriteLine($"进程 {process.ProcessName} (ID: {process.Id}) 已被关闭");
webApiProcess.Kill();
notifyIcon.Dispose();
System.Windows.Application.Current.Shutdown();
return;
}
}
}
}
catch (Exception ex)
{
MessageBox.Show($"无法访问进程 {process.ProcessName} (ID: {process.Id}) 的信息: {ex.Message}");
}
}
}
else
{
MessageBox.Show("用户名或密码错误,请重试。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
};
dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
dialog.ShowDialog();
}
至于如何更加优雅的关闭,还请读者们发挥思路,给出合理答案,谢谢!
4万+

被折叠的 条评论
为什么被折叠?



