wpf dependencyproperty

本次文章接上次写的“基础版”继续 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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Text;
  6. namespace WpfApplication2
  7. {
  8. public class Message : INotifyPropertyChanged
  9. {
  10. private string _content;
  11. public string Content
  12. {
  13. get { return _content; }
  14. set
  15. {
  16. if (_content != value)
  17. {
  18. _content = value;
  19. OnPropertyChanged(nameof(Content)); // 通知UI更新
  20. }
  21. }
  22. }
  23. public bool IsAI { get; set; } // 标记消息是否来自AI
  24. public bool IsUser { get; set; } // 标记消息是否来自用户
  25. public event PropertyChangedEventHandler PropertyChanged;
  26. protected virtual void OnPropertyChanged(string propertyName)
  27. {
  28. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  29. }
  30. }
  31. }

MainWindow.xaml

  1. <Window x:Class="WpfApplication2.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6. xmlns:local="clr-namespace:WpfApplication2"
  7. mc:Ignorable="d"
  8. Title="DeepSeek客户端" Height="680" Width="850">
  9. <Window.Resources>
  10. <!-- Boolean to Visibility Converter -->
  11. <local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
  12. </Window.Resources>
  13. <Window.DataContext>
  14. <local:ChatViewModel/>
  15. </Window.DataContext>
  16. <Grid>
  17. <Grid.RowDefinitions>
  18. <RowDefinition Height="8.5*"/>
  19. <RowDefinition Height="1.5*"/>
  20. </Grid.RowDefinitions>
  21. <!--第一个格子,AI对话格子-->
  22. <Grid Grid.Row="0" Grid.Column="0" Margin="0,15,0,0">
  23. <ListBox Name="listbox" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Messages}" Margin="0,-20,0,-14">
  24. <ListBox.ItemTemplate>
  25. <DataTemplate>
  26. <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
  27. <!-- AI消息 -->
  28. <StackPanel Orientation="Horizontal" Visibility="{Binding IsAI, Converter={StaticResource BooleanToVisibilityConverter}}">
  29. <Image Source="/Resources/Deepseek.png" Width="40" Height="40" Margin="5" VerticalAlignment="Top" />
  30. <!-- 使用TextBox代替TextBlock,并设置为只读 -->
  31. <TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" />
  32. </StackPanel>
  33. <!-- 用户消息 -->
  34. <StackPanel Orientation="Horizontal" Visibility="{Binding IsUser, Converter={StaticResource BooleanToVisibilityConverter}}">
  35. <Image Source="/Resources/User.png" Width="40" Height="40" Margin="5" VerticalAlignment="Top" />
  36. <!-- 使用TextBox代替TextBlock,并设置为只读 -->
  37. <TextBox Text="{Binding Content}" FontFamily="Segoe UI" FontSize="16" Padding="5,10" TextWrapping="Wrap" MaxWidth="750" VerticalAlignment="Top" IsReadOnly="True" BorderBrush="Transparent" Background="Transparent" />
  38. </StackPanel>
  39. </StackPanel>
  40. </DataTemplate>
  41. </ListBox.ItemTemplate>
  42. </ListBox>
  43. </Grid>
  44. <!--第二个格子,用户输入框-->
  45. <Grid Grid.Row="1" Grid.Column="0">
  46. <Grid.ColumnDefinitions>
  47. <ColumnDefinition Width="3*" />
  48. <!-- 调整比例为3:1,更符合输入框和按钮的实际需求 -->
  49. <ColumnDefinition Width="1*"/>
  50. </Grid.ColumnDefinitions>
  51. <!-- 输入信息框 -->
  52. <Grid Grid.Column="0" Margin="0,0,0,0">
  53. <!-- 统一化Margin值 -->
  54. <TextBox x:Name="InputTextBox"
  55. MaxWidth="540"
  56. Height="50"
  57. VerticalAlignment="Bottom"
  58. KeyDown="InputTextBox_KeyDown"
  59. Margin="107,0,2.4,19.6"/>
  60. <!-- 移除内层Margin,使用Grid的Margin控制 -->
  61. </Grid>
  62. <!-- 发送按钮区域 -->
  63. <Grid Grid.Column="1" Margin="0,0,0,0">
  64. <!-- 添加右下Margin保持整体平衡 -->
  65. <!-- 发送按钮 -->
  66. <Button x:Name="SendButton"
  67. Content="Send"
  68. Width="70"
  69. Height="40"
  70. HorizontalAlignment="Left"
  71. VerticalAlignment="Bottom"
  72. Background="#147bc6"
  73. Foreground="White"
  74. Click="SendButton_Click"
  75. FontFamily="Arial Black"
  76. FontSize="13"
  77. Margin="6,0,0,23.6"/>
  78. <Button x:Name="SendButton1"
  79. Content="new"
  80. Width="30"
  81. Height="30"
  82. HorizontalAlignment="Left"
  83. VerticalAlignment="Bottom"
  84. Background="#FFB6F5C2"
  85. Foreground="#FF424234"
  86. Click="SendButton_Click1"
  87. FontFamily="Cambria"
  88. Margin="93,0,0,49"/>
  89. </Grid>
  90. </Grid>
  91. </Grid>
  92. </Window>

MainWindow.xaml.cs

  1. using Newtonsoft.Json;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Text.RegularExpressions;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using System.Windows;
  11. using System.Windows.Controls;
  12. using System.Windows.Data;
  13. using System.Windows.Documents;
  14. using System.Windows.Input;
  15. using System.Windows.Media;
  16. using System.Windows.Media.Imaging;
  17. using System.Windows.Navigation;
  18. using System.Windows.Shapes;
  19. using System.Net;
  20. using System.Threading;
  21. namespace WpfApplication2
  22. {
  23. ///
  24. /// MainWindow.xaml 的交互逻辑
  25. ///
  26. public partial class MainWindow : Window
  27. {
  28. // 创建一个ChatViewModel对象来保存聊天历史
  29. private ChatViewModel _viewModel;
  30. // 用于存储对话的历史记录
  31. static StringBuilder conversationHistory = new StringBuilder();
  32. public MainWindow()
  33. {
  34. InitializeComponent();
  35. _viewModel = new ChatViewModel();
  36. DataContext = _viewModel;
  37. }
  38. ///
  39. /// 输入按钮框
  40. ///
  41. ///
  42. ///
  43. private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
  44. {
  45. // 用户输入
  46. string userInput = InputTextBox.Text;
  47. if (e.Key == Key.Enter)
  48. {
  49. // 异步方法需在同步上下文中调用(需手动处理)
  50. Task.Factory.StartNew(() =>
  51. {
  52. // 调用同步的AIMain方法获取响应
  53. RunAI(userInput);
  54. });
  55. clearText();
  56. }
  57. }
  58. ///
  59. /// 将最新的消息显示到最上面
  60. ///
  61. private void clearText()
  62. {
  63. // 设置最后一个消息为选中的项
  64. listbox.SelectedItem = _viewModel.Messages.LastOrDefault();
  65. // 滚动到选中的项(即最后一项)
  66. listbox.ScrollIntoView(listbox.SelectedItem);
  67. }
  68. ///
  69. /// 确认发送按钮
  70. ///
  71. ///
  72. ///
  73. private void SendButton_Click(object sender, RoutedEventArgs e)
  74. {
  75. // 用户输入
  76. string userInput = InputTextBox.Text;
  77. // 异步方法需在同步上下文中调用(需手动处理)
  78. Task.Factory.StartNew(() =>
  79. {
  80. // 调用同步的AIMain方法获取响应
  81. RunAI(userInput);
  82. });
  83. clearText();
  84. }
  85. private CancellationTokenSource cancellationTokenSource;
  86. private CancellationToken cancellationToken;
  87. public void RunAI(string userInput)
  88. {
  89. // 如果输入不正确,不输出
  90. if (string.IsNullOrWhiteSpace(userInput))
  91. return;
  92. // 创建取消源
  93. cancellationTokenSource = new CancellationTokenSource();
  94. cancellationToken = cancellationTokenSource.Token;
  95. // 用户输入添加到历史对话记录
  96. conversationHistory.AppendLine($"用户: {userInput}");
  97. // 添加用户消息
  98. Dispatcher.Invoke((Action)(() =>
  99. {
  100. // 添加AI消息
  101. _viewModel.AddUserMessage(userInput);
  102. }));
  103. // 用户输入添加到历史对话记录
  104. var requestData = new
  105. {
  106. model = "deepseek-r1:1.5b",
  107. prompt = conversationHistory.ToString(),
  108. stream = true
  109. };
  110. string jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(requestData);
  111. byte[] byteArray = Encoding.UTF8.GetBytes(jsonContent);
  112. HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://localhost:11434/api/generate");
  113. request.Method = "POST";
  114. request.ContentType = "application/json";
  115. request.ContentLength = byteArray.Length;
  116. try
  117. {
  118. using (Stream dataStream = request.GetRequestStream())
  119. {
  120. dataStream.Write(byteArray, 0, byteArray.Length);
  121. }
  122. }
  123. catch
  124. {
  125. MessageBox.Show("请本地配置DeepSeek,或者启动相关服务");
  126. return;
  127. }
  128. try
  129. {
  130. using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
  131. using (Stream responseStream = response.GetResponseStream())
  132. using (StreamReader reader = new StreamReader(responseStream))
  133. {
  134. string line;
  135. string line2 = "";
  136. while ((line = reader.ReadLine()) != null)
  137. {
  138. // 检查取消标志
  139. if (cancellationToken.IsCancellationRequested)
  140. {
  141. break; // 如果取消请求,退出读取流
  142. }
  143. if (!string.IsNullOrEmpty(line))
  144. {
  145. dynamic result = Newtonsoft.Json.JsonConvert.DeserializeObject(line);
  146. if (result != null && result.response != null)
  147. {
  148. // 每次读取一行后,立即通过Dispatcher更新UI
  149. string responseText = result.response;
  150. // 去除所有多余的换行符(例如将每个换行符替换为空格)
  151. responseText = responseText.Replace(Environment.NewLine, " ");
  152. string surrt = RegexLine(responseText);
  153. line2 += surrt;
  154. Dispatcher.Invoke((Action)(() =>
  155. {
  156. // 添加AI消息
  157. _viewModel.AddAIMessage(surrt);
  158. }));
  159. }
  160. }
  161. }
  162. //添加历史对话
  163. conversationHistory.AppendLine($"DeepSeek: {line2}");
  164. line2 = "";
  165. }
  166. }
  167. catch (WebException ex)
  168. {
  169. MessageBox.Show("请求异常: " + ex.Message);
  170. }
  171. Dispatcher.Invoke((Action)(() =>
  172. {
  173. // 清空输入框
  174. InputTextBox.Text = "";
  175. }));
  176. }
  177. ///
  178. /// 处理DeepSeek返回的字符串
  179. ///
  180. ///
  181. ///
  182. private string RegexLine(string line2)
  183. {
  184. // 使用正则表达式去掉 和 标签
  185. line2 = Regex.Replace(line2, @"", "\n");
  186. // 去掉开头的换行符
  187. line2 = line2.TrimStart('\r', '\n');
  188. return line2;
  189. }
  190. ///
  191. /// 开启新的对话
  192. ///
  193. ///
  194. ///
  195. private void SendButton_Click1(object sender, RoutedEventArgs e)
  196. {
  197. // 取消流接收
  198. cancellationTokenSource?.Cancel();
  199. // 1清空 _viewModel 中的消息记录
  200. _viewModel.Messages.Clear();
  201. // 2清空输入框
  202. InputTextBox.Text = "";
  203. // 3清空历史记录
  204. conversationHistory.Clear();
  205. }
  206. }
  207. }

ChatViewModel.cs

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Collections.ObjectModel;
  5. using System.Text;
  6. using System.ComponentModel;
  7. namespace WpfApplication2
  8. {
  9. public class ChatViewModel : INotifyPropertyChanged
  10. {
  11. private ObservableCollection<Message> _messages;
  12. public ObservableCollection<Message> Messages
  13. {
  14. get { return _messages; }
  15. set
  16. {
  17. _messages = value;
  18. OnPropertyChanged(nameof(Messages));
  19. }
  20. }
  21. public ChatViewModel()
  22. {
  23. Messages = new ObservableCollection<Message>();
  24. }
  25. // 添加用户消息
  26. public void AddUserMessage(string userInput)
  27. {
  28. Messages.Add(new Message { Content = userInput, IsUser = true, IsAI = false });
  29. }
  30. // 添加AI消息
  31. public void AddAIMessage(string newText)
  32. {
  33. // 检查是否已有消息,且最后一条消息是AI消息
  34. if (Messages.Any() && !Messages.Last().IsUser)
  35. {
  36. Messages.Last().Content += newText; // 追加流数据到最后一条消息
  37. OnPropertyChanged(nameof(Messages)); // 通知UI更新
  38. }
  39. else
  40. {
  41. // 如果没有消息或最后一条消息是用户消息,则创建新消息
  42. Messages.Add(new Message { Content = newText, IsUser = false, IsAI = true });
  43. }
  44. }
  45. // 实现INotifyPropertyChanged接口
  46. public event PropertyChangedEventHandler PropertyChanged;
  47. protected virtual void OnPropertyChanged(string propertyName)
  48. {
  49. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  50. }
  51. }
  52. }

BooleanToVisibilityConverter.cs

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Windows;
  7. using System.Windows.Data;
  8. namespace WpfApplication2
  9. {
  10. public class BooleanToVisibilityConverter : IValueConverter
  11. {
  12. public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  13. {
  14. return value is bool && (bool)value ? Visibility.Visible : Visibility.Collapsed;
  15. }
  16. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  17. {
  18. return value;
  19. }
  20. }
  21. }

5 需要安装System.Net.Http库

  1. Install-package System.Net.Http

6 相关图片如下

Resources/Deepseek.png

Resources/User.png

通过以上步骤,我们成功创建了一个进阶版的DeepSeek本地客户端,具备了实时对话、消息区分和文本复制等功能。随着对WPF和DeepSeek的深入了解,您可以进一步扩展功能,比如增加更多的用户交互方式和优化UI设计。希望本文对您在WPF开发和DeepSeek应用方面有所帮助!

大数据

git -学习笔记

2025-3-3 10:15:54

大数据

kafka中isr啥意思(kafka的isr是什么)

2025-3-3 10:15:57

0 条回复 A文章作者 M管理员
欢迎您,新朋友,感谢参与互动!
    暂无讨论,说说你的看法吧