WPF创建DeepSeek本地自己的客户端-进阶版

本次文章接上次写的"基础版"继续 WPF快速创建DeepSeek本地自己的客户端-基础思路版本

1 开发环境与工具

开发工具:VS 2015

开发环境:.Net 4.0

使用技术:WPF

本章内容:WPF实现一个进阶版的DeepSeek客户端。

效果图如下:

实现的功能:

1、实时接收DeepSeek回复的数据。

2、用户输入识别和AI回复识别使用不同的头像。

3、能够复制文字。

2 搭建本地DeepSeek环境

我参考的是一下几个教程:

1、DeepSeek本地搭建部署+搭建知识库+智能体详细图文教程

2、【问题记录】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应用方面有所帮助!

相关推荐
图图图图爱睡觉44 分钟前
用大白话解释搜索引擎Elasticsearch是什么,有什么用,怎么用
大数据·elasticsearch·搜索引擎
B站计算机毕业设计超人1 小时前
计算机毕业设计Python+DeepSeek-R1大模型考研院校推荐系统 考研分数线预测 考研推荐系统 考研(源码+文档+PPT+讲解)
大数据·python·毕业设计·课程设计·数据可视化·推荐算法·毕设
哲讯智能科技1 小时前
MES:开启生产制造优秀管理新时代
大数据
补三补四2 小时前
因子有效性的审判使者——回测分析【量化实践】
大数据·人工智能·算法·金融·数据分析
每天瞎忙的农民工2 小时前
Doris、ClickHouse 和 Flink 这三个技术典型的应用场景
大数据·clickhouse·flink·doris
开利网络3 小时前
搭建数字化生态平台公司:痛点与蚓链解决方案
大数据·运维·人工智能·搜索引擎·信息可视化
狮歌~资深攻城狮3 小时前
Flink与Spark对比:大数据领域的“双雄争霸
大数据
kngines3 小时前
【实战 ES】实战 Elasticsearch:快速上手与深度实践-1.3.2Kibana可视化初探
大数据·elasticsearch·搜索引擎
Zack_wzm3 小时前
基于Ubuntu系统的Hadoop和Spark集群搭建(保姆级教程)
linux·hadoop·ubuntu·spark