本次文章接上次写的"基础版"继续 WPF快速创建DeepSeek本地自己的客户端-基础思路版本
1 开发环境与工具
开发工具:VS 2015
开发环境:.Net 4.0
使用技术:WPF
本章内容:WPF实现一个进阶版的DeepSeek客户端。
效果图如下:
实现的功能:
1、实时接收DeepSeek回复的数据。
2、用户输入识别和AI回复识别使用不同的头像。
3、能够复制文字。
2 搭建本地DeepSeek环境
我参考的是一下几个教程:
1、DeepSeek本地搭建部署+搭建知识库+智能体详细图文教程
3、公司数据不泄露,DeepSeek R1本地化部署+web端访问+个人知识库搭建与使用,喂饭级实操教程,老旧笔记本竟跑出企业级AI
4、【大语言模型】本地快速部署Ollama运行大语言模型详细流程
3 vs2015 创建WPF项目
Message.cs
csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace WpfApplication2
{
public class Message : INotifyPropertyChanged
{
private string _content;
public string Content
{
get { return _content; }
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged(nameof(Content)); // 通知UI更新
}
}
}
public bool IsAI { get; set; } // 标记消息是否来自AI
public bool IsUser { get; set; } // 标记消息是否来自用户
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainWindow.xaml
csharp
<Window x:Class="WpfApplication2.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:WpfApplication2"
mc:Ignorable="d"
Title="DeepSeek客户端" Height="680" Width="850">
<Window.Resources>
<!-- Boolean to Visibility Converter -->
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<Window.DataContext>
<local:ChatViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8.5*"/>
<RowDefinition Height="1.5*"/>
</Grid.RowDefinitions>
<!--第一个格子,AI对话格子-->
<Grid Grid.Row="0" Grid.Column="0" Margin="0,15,0,0">
<ListBox Name="listbox" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Messages}" Margin="0,-20,0,-14">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<!-- AI消息 -->
<StackPanel Orientation="Horizontal" Visibility="{Binding IsAI, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image Source="/Resources/Deepseek.png" Width="40" Height="40" Margin="5" VerticalAlignment="Top" />
<!-- 使用TextBox代替TextBlock,并设置为只读 -->
<TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" />
</StackPanel>
<!-- 用户消息 -->
<StackPanel Orientation="Horizontal" Visibility="{Binding IsUser, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image Source="/Resources/User.png" Width="40" Height="40" Margin="5" VerticalAlignment="Top" />
<!-- 使用TextBox代替TextBlock,并设置为只读 -->
<TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<!--第二个格子,用户输入框-->
<Grid Grid.Row="1" Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<!-- 调整比例为3:1,更符合输入框和按钮的实际需求 -->
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 输入信息框 -->
<Grid Grid.Column="0" Margin="0,0,0,0">
<!-- 统一化Margin值 -->
<TextBox x:Name="InputTextBox"
MaxWidth="540"
Height="50"
VerticalAlignment="Bottom"
KeyDown="InputTextBox_KeyDown"
Margin="107,0,2.4,19.6"/>
<!-- 移除内层Margin,使用Grid的Margin控制 -->
</Grid>
<!-- 发送按钮区域 -->
<Grid Grid.Column="1" Margin="0,0,0,0">
<!-- 添加右下Margin保持整体平衡 -->
<!-- 发送按钮 -->
<Button x:Name="SendButton"
Content="Send"
Width="70"
Height="40"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Background="#147bc6"
Foreground="White"
Click="SendButton_Click"
FontFamily="Arial Black"
FontSize="13"
Margin="6,0,0,23.6"/>
<Button x:Name="SendButton1"
Content="new"
Width="30"
Height="30"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Background="#FFB6F5C2"
Foreground="#FF424234"
Click="SendButton_Click1"
FontFamily="Cambria"
Margin="93,0,0,49"/>
</Grid>
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
csharp
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.IO;
using System.Linq;
using System.Net.Http;
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.Net;
using System.Threading;
namespace WpfApplication2
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
// 创建一个ChatViewModel对象来保存聊天历史
private ChatViewModel _viewModel;
// 用于存储对话的历史记录
static StringBuilder conversationHistory = new StringBuilder();
public MainWindow()
{
InitializeComponent();
_viewModel = new ChatViewModel();
DataContext = _viewModel;
}
/// <summary>
/// 输入按钮框
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
{
// 用户输入
string userInput = InputTextBox.Text;
if (e.Key == Key.Enter)
{
// 异步方法需在同步上下文中调用(需手动处理)
Task.Factory.StartNew(() =>
{
// 调用同步的AIMain方法获取响应
RunAI(userInput);
});
clearText();
}
}
/// <summary>
/// 将最新的消息显示到最上面
/// </summary>
private void clearText()
{
// 设置最后一个消息为选中的项
listbox.SelectedItem = _viewModel.Messages.LastOrDefault();
// 滚动到选中的项(即最后一项)
listbox.ScrollIntoView(listbox.SelectedItem);
}
/// <summary>
/// 确认发送按钮
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SendButton_Click(object sender, RoutedEventArgs e)
{
// 用户输入
string userInput = InputTextBox.Text;
// 异步方法需在同步上下文中调用(需手动处理)
Task.Factory.StartNew(() =>
{
// 调用同步的AIMain方法获取响应
RunAI(userInput);
});
clearText();
}
private CancellationTokenSource cancellationTokenSource;
private CancellationToken cancellationToken;
public void RunAI(string userInput)
{
// 如果输入不正确,不输出
if (string.IsNullOrWhiteSpace(userInput))
return;
// 创建取消源
cancellationTokenSource = new CancellationTokenSource();
cancellationToken = cancellationTokenSource.Token;
// 用户输入添加到历史对话记录
conversationHistory.AppendLine($"用户: {userInput}");
// 添加用户消息
Dispatcher.Invoke((Action)(() =>
{
// 添加AI消息
_viewModel.AddUserMessage(userInput);
}));
// 用户输入添加到历史对话记录
var requestData = new
{
model = "deepseek-r1:1.5b",
prompt = conversationHistory.ToString(),
stream = true
};
string jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(requestData);
byte[] byteArray = Encoding.UTF8.GetBytes(jsonContent);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://localhost:11434/api/generate");
request.Method = "POST";
request.ContentType = "application/json";
request.ContentLength = byteArray.Length;
try
{
using (Stream dataStream = request.GetRequestStream())
{
dataStream.Write(byteArray, 0, byteArray.Length);
}
}
catch
{
MessageBox.Show("请本地配置DeepSeek,或者启动相关服务");
return;
}
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
using (Stream responseStream = response.GetResponseStream())
using (StreamReader reader = new StreamReader(responseStream))
{
string line;
string line2 = "";
while ((line = reader.ReadLine()) != null)
{
// 检查取消标志
if (cancellationToken.IsCancellationRequested)
{
break; // 如果取消请求,退出读取流
}
if (!string.IsNullOrEmpty(line))
{
dynamic result = Newtonsoft.Json.JsonConvert.DeserializeObject(line);
if (result != null && result.response != null)
{
// 每次读取一行后,立即通过Dispatcher更新UI
string responseText = result.response;
// 去除所有多余的换行符(例如将每个换行符替换为空格)
responseText = responseText.Replace(Environment.NewLine, " ");
string surrt = RegexLine(responseText);
line2 += surrt;
Dispatcher.Invoke((Action)(() =>
{
// 添加AI消息
_viewModel.AddAIMessage(surrt);
}));
}
}
}
//添加历史对话
conversationHistory.AppendLine($"DeepSeek: {line2}");
line2 = "";
}
}
catch (WebException ex)
{
MessageBox.Show("请求异常: " + ex.Message);
}
Dispatcher.Invoke((Action)(() =>
{
// 清空输入框
InputTextBox.Text = "";
}));
}
/// <summary>
/// 处理DeepSeek返回的字符串
/// </summary>
/// <param name="line2"></param>
/// <returns></returns>
private string RegexLine(string line2)
{
// 使用正则表达式去掉 <think> 和 </think> 标签
line2 = Regex.Replace(line2, @"<\/?think>", "\n");
// 去掉开头的换行符
line2 = line2.TrimStart('\r', '\n');
return line2;
}
/// <summary>
/// 开启新的对话
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SendButton_Click1(object sender, RoutedEventArgs e)
{
// 取消流接收
cancellationTokenSource?.Cancel();
// 1清空 _viewModel 中的消息记录
_viewModel.Messages.Clear();
// 2清空输入框
InputTextBox.Text = "";
// 3清空历史记录
conversationHistory.Clear();
}
}
}
ChatViewModel.cs
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.Text;
using System.ComponentModel;
namespace WpfApplication2
{
public class ChatViewModel : INotifyPropertyChanged
{
private ObservableCollection<Message> _messages;
public ObservableCollection<Message> Messages
{
get { return _messages; }
set
{
_messages = value;
OnPropertyChanged(nameof(Messages));
}
}
public ChatViewModel()
{
Messages = new ObservableCollection<Message>();
}
// 添加用户消息
public void AddUserMessage(string userInput)
{
Messages.Add(new Message { Content = userInput, IsUser = true, IsAI = false });
}
// 添加AI消息
public void AddAIMessage(string newText)
{
// 检查是否已有消息,且最后一条消息是AI消息
if (Messages.Any() && !Messages.Last().IsUser)
{
Messages.Last().Content += newText; // 追加流数据到最后一条消息
OnPropertyChanged(nameof(Messages)); // 通知UI更新
}
else
{
// 如果没有消息或最后一条消息是用户消息,则创建新消息
Messages.Add(new Message { Content = newText, IsUser = false, IsAI = true });
}
}
// 实现INotifyPropertyChanged接口
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
BooleanToVisibilityConverter.cs
csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication2
{
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool && (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
}
5 需要安装System.Net.Http库
bash
Install-package System.Net.Http
6 相关图片如下
Resources/Deepseek.png
Resources/User.png
通过以上步骤,我们成功创建了一个进阶版的DeepSeek本地客户端,具备了实时对话、消息区分和文本复制等功能。随着对WPF和DeepSeek的深入了解,您可以进一步扩展功能,比如增加更多的用户交互方式和优化UI设计。希望本文对您在WPF开发和DeepSeek应用方面有所帮助!