前言
C#实现本地AI聊天功能
- WPF+OllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。
此程序默认你已经安装好了Ollama。
在运行前需要线安装好Ollama,如何安装请自行搜索
Ollama下载地址: https://ollama.org.cn
Ollama模型下载地址: https://ollama.org.cn/library
基本运行环境: 根据自己使用的AI搜索对应模型基本配置,有需要使用GPU运行的模型。
- 此程序除了安装Ollama外,无需安装其他配置。
3、相关依赖

运行

项目
项目结构
- 项目结构包含如下目录:
具体内容如下图:

项目代码
Commands
EventsCommand
using System.Windows.Input;
/// <summary>
/// 事件命令:
/// 有些控件的无法绑定命令,但是想要实现命令绑定功能,可通过创建该命令实现。
/// 需要引用Microsoft.Xaml.Behaviors.Wpf组合实现。
/// </summary>
public class EventsCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public EventsCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke((T)parameter) ?? true;
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
ParameterCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{
/// <summary>
/// 参数命令:
/// 可以带参数的命令:
/// </summary>
public class ParameterCommand : ICommand
{
public Action<object> execute;
public ParameterCommand(Action<object> execute)
{
this.execute = execute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return CanExecuteChanged != null;
}
public void Execute(object? parameter)
{
execute?.Invoke(parameter);
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
ParameterlessCommand
using System.Windows.Input;
namespace OfflineAI.Commands
{
/// <summary>
/// 无参数命令:
/// 无参数的命令:
/// </summary>
public class ParameterlessCommand : ICommand
{
private Action _execute;
public ParameterlessCommand(Action execute)
{
_execute = execute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return CanExecuteChanged != null;
}
public void Execute(object? parameter)
{
_execute.Invoke();
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
Models
ChatRecordModel
namespace OfflineAI.Models
{
/// <summary>
/// 聊天记录模型
/// </summary>
public class ChatRecordModel
{
public ChatRecordModel(int id, string dateTime, string name,string fullName, string data)
{
Id = id;
DateTime = dateTime;
Name = name;
FullName = fullName;
Data = data;
}
/// <summary>
/// ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 日期
/// </summary>
public string DateTime { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 完整名称
/// </summary>
public string FullName { get; set; }
/// <summary>
/// 数据
/// </summary>
public string Data { get; set; }
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
FileOperationModel
namespace OfflineAI.Models
{
public class FileOperationModel
{
/// <summary>
/// 是否生成目录
/// </summary>
public bool IsGenerateDirectory { get; set; }
/// <summary>
/// 文件目录
/// </summary>
public string Directory { get; set; }
/// <summary>
/// 日期目录(生成的目录)
/// </summary>
public string DirectoryDateTime { get; set; }
/// <summary>
/// 文件名称(全路径)
/// </summary>
public string FileName { get; set; }
/// <summary>
/// 文件名称(生成文件全路径)
/// </summary>
public string FileNameDateTime { get; set; }
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
Services
FileOperation
using OfflineAI.Models;
using System.IO;
namespace OfflineAI.Services
{
/// <summary>
/// 文件操作类:
/// 1、2025-02-24:添加创建日期目录方法。输入文件名,添加时间目录。
/// 2、2025-02-24:添加写入数据到文件方法(.txt格式)
/// </summary>
public class FileOperation
{
private FileOperationModel _fileOperation;
#region 构造函数
public FileOperation(string fileName)
{
_fileOperation = new FileOperationModel();
_fileOperation.IsGenerateDirectory = true;
UpdataFileName(fileName);
}
#endregion
#region 公共方法
/// <summary>
/// 更新文件名
/// </summary>
public void UpdataFileName(string fileName)
{
if (Path.GetExtension(fileName).ToLower().Equals("txt"))
_fileOperation.FileName = fileName;
else
_fileOperation.FileName = fileName + ".txt";
_fileOperation.Directory = Path.GetDirectoryName(fileName);
CreateDateTime();
_fileOperation.FileNameDateTime = $"{_fileOperation.DirectoryDateTime}\\{Path.GetFileName(_fileOperation.FileName)}";
}
/// <summary>
/// 写入文本
/// </summary>
public void WriteTxt(string data)
{
SaveDataAsTxt(data);
}
/// <summary>
/// 写入文本,指定文件名
/// </summary>
public void WriteTxt(string fileName, string data)
{
UpdataFileName(fileName);
SaveDataAsTxt(data);
}
public string ReadTxt(string fileName)
{
// 使用 using 语句确保资源被正确释放
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
using (StreamReader sr = new StreamReader(fs))
{
return sr.ReadToEnd();
}
}
/// <summary>
/// 获取指定目录下的所有文件(*.txt)
/// </summary>
public string[] GetFiles()
{
string[] files = Directory.GetFiles(_fileOperation.Directory, "*.txt", SearchOption.AllDirectories);
return files;
}
/// <summary>
/// 获取指定目录下的所有文件(*.txt)
/// </summary>
public static string[] GetFiles(string directory)
{
string[] files = Directory.GetFiles(directory, "*.txt", SearchOption.AllDirectories);
return files;
}
#endregion
#region 私有方法
/// <summary>
/// 保存数据为Txt类型的文本
/// </summary>
private void SaveDataAsTxt(string data)
{
if (_fileOperation.IsGenerateDirectory)
{
try
{
string fileName = _fileOperation.FileName;
if (_fileOperation.IsGenerateDirectory)
{
fileName = _fileOperation.FileNameDateTime;
}
using (FileStream fileStream = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
{
using (StreamWriter writer = new StreamWriter(fileStream))
{
writer.Write(data);
}
}
Console.WriteLine("数据已成功写入文件。");
}
catch (Exception ex)
{
Console.WriteLine("写入文件时发生错误: " + ex.Message);
}
}
}
/// <summary>
/// 创建日期目录
/// </summary>
private void CreateDateTime()
{
if (_fileOperation.IsGenerateDirectory)
{
string path = $"{_fileOperation.Directory}\\{DateTime.Now.ToString("yyyy")}";
Directory.CreateDirectory($"{path}");
path = $"{path}\\{DateTime.Now.ToString("yyyyMMdd")}\\";
Directory.CreateDirectory($"{path}");
_fileOperation.DirectoryDateTime = path;
}
}
#endregion
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
ProcessService
using System.ComponentModel;
using System.Diagnostics;
namespace OfflineAI.Services
{
public class ProcessService
{
/// <summary>
/// 执行CMD指令
/// </summary>
public static bool ExecuteCommand(string command)
{
// 创建一个新的进程启动信息
ProcessStartInfo processStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe", // 设置要启动的程序为cmd.exe
Arguments = $"/C {command}", // 设置要执行的命令
UseShellExecute = true, // 使用操作系统shell启动进程
CreateNoWindow = false, //不创建窗体
};
try
{
Process process = Process.Start(processStartInfo);// 启动进程
process.WaitForExit(); // 等待进程退出
process.Close(); // 返回是否成功执行
return process.ExitCode == 0;
}
catch (Exception ex)
{
Debug.WriteLine($"发生错误: {ex.Message}");// 其他异常处理
return false;
}
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
ShareOllamaObject
using OfflineAI.Services;
using OllamaSharp;
using System.Collections.ObjectModel;
namespace OfflineAI.Sevices
{
/// <summary>
/// 共享Ollama对象类:保持Ollama对象一致才能使用当前对象实现对话
/// 作 者:吾与谁归
/// 时 间:2025年02月18日
/// 功 能:
/// 1) 2025-02-18:使用cmd命令启动Ollama服务,目前使用ollama list();
/// 2) 2025-02-18:初始化模型参数,在初始化时启用GPU、连接ollama、初始化模型。
/// </summary>
public class ShareOllamaObject
{
#region 字段|属性|集合
#region 字段
private bool _connected = false; //连接状态
private Chat chat; //构建交互式聊天模型对象。
private OllamaApiClient _ollama; //OllamaAPI对象
private string _selectModel; //选择的模型名称
#endregion
#region 属性
/// <summary>
/// 连接状态
/// </summary>
public bool Connected
{
get { return _connected; }
set { _connected = value; }
}
public string SelectModel { get => _selectModel; set => _selectModel = value; }
/// <summary>
/// 构建交互式聊天模型对象。
/// </summary>
public Chat Chat
{
get { return chat; }
set { chat = value; }
}
/// <summary>
/// OllamaAPI对象
/// </summary>
public OllamaApiClient Ollama
{
get { return _ollama; }
set { _ollama = value; }
}
#endregion
#region 集合
/// <summary>
/// 模型列表
/// </summary>
public ObservableCollection<string> ModelList { get; set; }
#endregion
#endregion
#region 构造函数
public ShareOllamaObject()
{
ProcessService.ExecuteCommand("ollama list");
Initialize("llama3.2:3b");
ProcessService.GetProcessId("ollama");
}
#endregion
#region 其他方法
/// <summary>
/// 初始化方法
/// </summary>
private void Initialize( string modelName)
{
try
{
// 设置默认设备为GPU
Environment.SetEnvironmentVariable("OLLAMA_DEFAULT_DEVICE", "gpu");
//连接Ollama,并设置初始模型
Ollama = new OllamaApiClient(new Uri("http://localhost:11434"));
//获取本地可用的模型列表
ModelList = (ObservableCollection<string>)GetModelList();
//遍历查找是否包含llama3.2:3b模型
var tmepModelName = ModelList.FirstOrDefault(name => name.ToLower().Contains("llama3.2:3b"));
//设置的模型不为空
if (tmepModelName != null)
{
Ollama.SelectedModel = tmepModelName;
}
//模型列表不为空
else if (ModelList.Count > 0)
{
_ollama.SelectedModel = ModelList[ModelList.Count - 1];
}
//Ollama服务启用成功
SelectModel = _ollama.SelectedModel;
_connected = true;
chat = new Chat(_ollama);
}
catch (Exception)
{
_connected = false; //Ollama服务启用失败
}
}
/// <summary>
/// 获取模型里列表
/// </summary>
public Collection<string> GetModelList()
{
var models = _ollama.ListLocalModelsAsync();
var modelList = new ObservableCollection<string>();
foreach (var model in models.Result)
{
modelList.Add(model.Name);
}
return modelList;
}
public void ReCreateChat()
{
chat = new Chat(_ollama);
}
#endregion
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
ViewModels
MainViewModel
using OfflineAI.Sevices;
using OfflineAI.Commands;
using OfflineAI.Views;
using System.Windows;
using System.Diagnostics;
using System.Windows.Input;
using System.ComponentModel;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.IO;
using OfflineAI.Services;
using OfflineAI.Models;
namespace OfflineAI.ViewModels
{
/// <summary>
/// 主窗体视图模型:
/// 作者:吾与谁归
/// 时间:2025年02月17日(首次创建时间)
/// 更新:
/// 1、2025-02-17:添加折叠栏展开|折叠功能。
/// 2、2025-02-17:视图切换功能 1)系统设置 2) 聊天
/// 3、2025-02-18:关闭窗体时提示是否关闭,释放相关资源。
/// 4、2025-02-19:添加首页功能,和修改新聊天功能。点击新聊天会创建新的会话(Chat)。
/// 5、2025-02-20:窗体加载时传递Ollama对象。
/// 6、2025-02-24:添加了窗体加载时,加载聊天记录的功能。
/// </summary>
public class MainViewModel : PropertyChangedBase
{
#region 字段、属性、集合、命令
#region 字段
private UserControl _currentView; //当前视图
private ShareOllamaObject _ollamaService; //共享Ollama服务对象
private string _selectedModel; //选择的模型
private ObservableCollection<string> _modelListCollection; //模型列表
private int _expandedBarWidth = 50; //折叠栏宽度
private string _directory; //目录
private string _fileName; //文件
private ObservableCollection<ChatRecordModel> _chatRecordCollection;
public event Action<string> LoadChatRecordEventHandler;
#endregion
#region 属性
/// <summary>
/// 当前显示视图
/// </summary>
public UserControl CurrentView {
get => _currentView;
set
{
if (_currentView != value)
{
_currentView = value;
OnPropertyChanged();
}
}
}
public ShareOllamaObject OllamaService
{
get => _ollamaService;
set
{
if (_ollamaService != value)
{
_ollamaService = value;
OnPropertyChanged();
}
}
}
public string SelectedModel
{
get => _selectedModel;
set
{
if (_selectedModel != value)
{
_selectedModel = value;
OllamaService.Ollama.SelectedModel = value;
OllamaService.Chat.Model = value;
OnPropertyChanged();
}
}
}
public int ExpandedBarWidth
{
get => _expandedBarWidth;
set
{
if (_expandedBarWidth != value)
{
_expandedBarWidth = value;
OnPropertyChanged();
}
}
}
#endregion
#region 集合
/// <summary>
/// 视图集合,保存视图
/// </summary>
public ObservableCollection<UserControl> ViewCollection { get; set; }
public ObservableCollection<string> ModelListCollection
{
get => _modelListCollection;
set
{
if (_modelListCollection != value)
{
_modelListCollection = value;
OnPropertyChanged();
}
}
}
public ObservableCollection<ChatRecordModel> ChatRecordCollection
{
get => _chatRecordCollection;
set
{
if (_chatRecordCollection != value)
{
_chatRecordCollection = value;
OnPropertyChanged();
}
}
}
#endregion
#region 命令
/// <summary>
/// 展开功能菜单命令
/// </summary>
public ICommand ExpandedMenuCommand { get; set; }
/// <summary>
/// 折叠功能菜单命令
/// </summary>
public ICommand CollapsedMenuCommand { get; set; }
/// <summary>
/// 切换视图命令
/// </summary>
public ICommand SwitchViewCommand { get; set; }
/// <summary>
/// 窗体关闭命令
/// </summary>
public ICommand ClosingWindowCommand { get; set; }
/// <summary>
/// 窗体加载命令
/// </summary>
public ICommand LoadedWindowCommand { get; set; }
/// <summary>
/// 聊天记录鼠标按下命令
/// </summary>
public ICommand ChatRecordMouseDownCommand { get; set; }
#endregion
#endregion
#region 构造函数
public MainViewModel()
{
Initialize();
}
/// <summary>
/// 初始化方法
/// </summary>
public void Initialize()
{
//初始化Ollama
_ollamaService = new ShareOllamaObject();
ModelListCollection = _ollamaService.ModelList;
SelectedModel = _ollamaService.SelectModel;
//创建命令
SwitchViewCommand = new ParameterCommand(SwitchViewTrigger);
LoadedWindowCommand = new EventsCommand<object>(LoadedWindowTrigger);
CollapsedMenuCommand = new EventsCommand<object>(CollapsedMenuTrigger);
ExpandedMenuCommand = new EventsCommand<object>(ExpandedMenuTrigger);
ClosingWindowCommand = new EventsCommand<object>(ClosingWindowTrigger);
ChatRecordMouseDownCommand = new EventsCommand<ChatRecordModel>(ChatRecordMouseDownTrigger);
ViewCollection = new ObservableCollection<UserControl>();
//添加视图到集合
ViewCollection.Add(new SystemSettingView());
ViewCollection.Add(new UserChatView());
//默认显示窗体
CurrentView = ViewCollection[1];
//折叠栏折叠状态
ExpandedBarWidth = 25;
//加载聊天记录
LoadChatRecord();
}
#endregion
#region 命令方法
/// <summary>
/// 聊天记录鼠标按下
/// </summary>
private void ChatRecordMouseDownTrigger(ChatRecordModel obj)
{
Debug.Print(obj.ToString());
OnLoadChatRecordCallBack(obj.FullName.ToString());
}
/// <summary>
/// 触发主视图窗体加载方法
/// </summary>
private void LoadedWindowTrigger(object sender)
{
Debug.Print(sender?.ToString());
var userView = ViewCollection.FirstOrDefault(obj => obj is UserChatView) as UserChatView;
userView.UserWindow.Ollama = _ollamaService;
LoadChatRecordEventHandler += userView.UserWindow.LoadChatRecordCallback;
}
/// <summary>
/// 触发关闭窗体方法
/// </summary>
private void ClosingWindowTrigger(object obj)
{
if (obj is CancelEventArgs cancelEventArgs)
{
if (MessageBox.Show("确定要关闭程序吗?", "确认关闭", MessageBoxButton.YesNo) == MessageBoxResult.No)
{
cancelEventArgs.Cancel = true; // 取消关闭
}
else
{
ClearingResources();
}
}
}
/// <summary>
/// 视图切换命令触发的方法
/// </summary>
private void SwitchViewTrigger(object obj)
{
Debug.WriteLine(obj.ToString());
switch (obj.ToString())
{
case "SystemSettingView":
CurrentView = ViewCollection[0];
break;
case "UserChatView":
CurrentView = ViewCollection[1];
break;
case "NewUserChatView":
UserChatView newChatView = new UserChatView();
OllamaService.ReCreateChat();
newChatView.UserWindow.Ollama = OllamaService;
ViewCollection[1] = newChatView;
CurrentView = newChatView;
break;
}
}
/// <summary>
/// 折叠菜单触发方法
/// </summary>
private void CollapsedMenuTrigger(object e)
{
ExpandedBarWidth = 25;
Debug.WriteLine("折叠");
}
/// <summary>
/// 展开菜单触发方法
/// </summary>
private void ExpandedMenuTrigger(object e)
{
ExpandedBarWidth = 250;
Debug.WriteLine("展开");
}
#endregion
#region 其他方法
/// <summary>
/// 加载聊天记录
/// </summary>
private void LoadChatRecord()
{
_directory = $"{Environment.CurrentDirectory}\\Record";
string[] files = FileOperation.GetFiles(_directory);
ObservableCollection<ChatRecordModel> records = new ObservableCollection<ChatRecordModel>();
string name = string.Empty;
string data = string.Empty;
foreach (var item in files)
{
name = Path.GetFileNameWithoutExtension(item);
data = File.ReadAllLines(item)[3];
if (data.Trim().Length > 1 )
{
records.Add(new ChatRecordModel(records.Count, name, name, item, data.Substring(1)));
}
}
ChatRecordCollection = records;
}
/// <summary>
/// 触发事件:加载聊天记录回调
/// </summary>
private void OnLoadChatRecordCallBack(object sender)
{
LoadChatRecordEventHandler.Invoke(sender.ToString());
}
/// <summary>
/// 释放资源:窗体关闭时触发
/// </summary>
private void ClearingResources()
{
//ProcessService.GetPIDAndCloseByPort(11434);
}
#endregion
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
PropertyChangedBase
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace OfflineAI.ViewModels
{
/// <summary>
/// 属性变更基类
/// </summary>
public class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
UserChatViewModel
using Markdig.Wpf;
using OfflineAI.Commands;
using OfflineAI.Services;
using OfflineAI.Sevices;
using System.Diagnostics;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
namespace OfflineAI.ViewModels
{
/// <summary>
/// 描述:用户聊天视图模型:
/// 作者:吾与谁归
/// 时间: 2025年2月19日
/// 更新:
/// 1、 2025-02-19:添加AI聊天功能,输出问题及结果到UI,并使用Markdown相关的库做简单渲染。
/// 2、 2025-02-20:优化了构造函数,使用无参构造,方便在设计器中直接绑定数据上下文(感觉)。
/// 3、 2025-02-20:滚轮滑动显示内容,提交问题后滚动显示内容,鼠标右键点击内容停止继续滚动,回答结束停止滚动。
/// 4、 2025-02-24:添加聊天记录保存功能。
/// 5、 2025-02-24:添加聊天记录加载功能,通过点击记录列表显示。
/// </summary>
public class UserChatViewModel:PropertyChangedBase
{
#region 字段、属性、集合、命令
#region 字段
private bool _isAutoScrolling = false; //是否自动滚动
private string _currentInputText; //当前输入文本
private string _messageContent; //消息内容
private string _directory; //目录
private string _fileName; //文件名
private MarkdownViewer _markdownViewer; //MarkdownViewer控件
private ScrollViewer _scrollViewer; //ScrollViewer滑动控件
private StringBuilder _message = new StringBuilder(); //消息字符串拼接
private CancellationToken cancellationToken; //异步线程取消标记
private FileOperation _fileIO; //文件IO
private ShareOllamaObject _ollama; //Ollama 对象实例
private string _submitButtonName;
#endregion
#region 属性
/// <summary>
/// 提交按钮名称
/// </summary>
public string SubmitButtonName
{
get => _submitButtonName;
set
{
if (_submitButtonName != value)
{
_submitButtonName = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// 消息内容
/// </summary>
public string? MessageContent
{
get => _messageContent;
set
{
_messageContent = value;
OnPropertyChanged();
}
}
/// <summary>
/// 当前输入文本
/// </summary>
public string CurrentInputText
{
get => _currentInputText;
set
{
if (_currentInputText != value)
{
_currentInputText = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// 共享Ollama对象
/// </summary>
public ShareOllamaObject Ollama
{
get => _ollama;
set
{
if (_ollama != value)
{
_ollama = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// 自动滚动消息
/// </summary>
public bool IsAutoScrolling
{
get => _isAutoScrolling;
set
{
if (_isAutoScrolling != value)
{
_isAutoScrolling = value;
OnPropertyChanged();
}
}
}
#endregion
#region 集合
#endregion
#region 命令
/// <summary>
/// 展开功能菜单命令
/// </summary>
public ICommand LoadFileCommand { get; set; }
/// <summary>
/// 提交命令
/// </summary>
public ICommand SubmiQuestionCommand { get; set; }
/// <summary>
/// 鼠标滚动
/// </summary>
public ICommand MouseWheelCommand { get; set; }
/// <summary>
/// 鼠标按下
/// </summary>
public ICommand MouseDownCommand { get; set; }
/// <summary>
/// Markdown对象命令
/// </summary>
public ICommand MarkdownOBJCommand { get; set; }
/// <summary>
/// 滑动条加载
/// </summary>
public ICommand ScrollLoadedCommand { get; set; }
#endregion
#endregion
#region 构造函数
public UserChatViewModel()
{
Initialize();
}
#endregion
#region 初始化方法
/// <summary>
/// 初始化方法
/// </summary>
public void Initialize()
{
//文件加载
LoadFileCommand = new ParameterCommand(LoadFileTrigger);
MouseWheelCommand = new EventsCommand<MouseWheelEventArgs>(MouseWheelTrigger);
MouseDownCommand = new EventsCommand<MouseButtonEventArgs>(MouseDownTrigger);
MarkdownOBJCommand = new EventsCommand<object>(MarkdownOBJTrigger);
SubmiQuestionCommand = new ParameterlessCommand(SubmitQuestionTrigger);
ScrollLoadedCommand = new EventsCommand<RoutedEventArgs>(ScrollLoadedTrigger);
//
SubmitButtonName = "提交";
//日志记录
_directory = $"{Environment.CurrentDirectory}\\Record\\";
_fileName = $"{_directory}\\{DateTime.Now.ToString("yyyyMMddHHmmss")}";
_fileIO = new FileOperation($"{_fileName}");
//
}
#endregion
#region 命令方法
/// <summary>
/// 加载文件
/// </summary>
private void LoadFileTrigger(object obj)
{
OpenFileDialog openFile = new OpenFileDialog();
openFile.Multiselect = true;
if (openFile.ShowDialog() == DialogResult.OK)
{
string[] files = openFile.FileNames;
if (files.Count() > 1)
{
foreach (var item in files)
{
Debug.WriteLine(item);
}
}
else
{
Debug.WriteLine(openFile.FileName);
}
}
}
/// <summary>
/// 提交: 提交问题到AI并获取返回结果
/// </summary>
private async void SubmitQuestionTrigger()
{
_ = Task.Delay(1);
string input = CurrentInputText;
try
{
if (!SubmintChecked(input)) return;
SubmitButtonName = "停止";
_message.Clear();
_isAutoScrolling = true;
AppendText($"##{Environment.NewLine}");
AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}]{Environment.NewLine}");
AppendText($"## 【User】{Environment.NewLine}");
AppendText($">{input}{Environment.NewLine}");
AppendText($"{Environment.NewLine}");
AppendText($"## 【AI】{Environment.NewLine}");
await foreach (var answerToken in Ollama.Chat.SendAsync(input))
{
AppendText(answerToken);
await Task.Delay(20);
if (_isAutoScrolling) _scrollViewer.ScrollToEnd();//是否自动滚动
}
AppendText($"{Environment.NewLine}{Environment.NewLine}");
}
catch (Exception ex)
{
AppendText($"Error: {ex.Message}");
AppendText($"{Environment.NewLine}{Environment.NewLine}");
}
//回答完成
_fileIO.WriteTxt($"{_fileName}", _message.ToString());
CurrentInputText = string.Empty;
_isAutoScrolling = false;
SubmitButtonName = "提交";
}
/// <summary>
/// 鼠标滚动上下滑动
/// </summary>
private void MouseWheelTrigger(MouseWheelEventArgs e)
{
try
{
// 获取 ScrollViewer 对象
if (e.Source is FrameworkElement element && element.Parent is ScrollViewer scrollViewer)
{
// 获取当前的垂直偏移量
double currentOffset = scrollViewer.VerticalOffset;
if (e.Delta > 0)
{
scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);
}
else
{
scrollViewer.ScrollToVerticalOffset(currentOffset - e.Delta);
}
// 标记事件已处理,防止默认滚动行为
e.Handled = true;
}
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}
}
/// <summary>
/// Markdown中鼠标按下
/// </summary>
private void MouseDownTrigger(MouseButtonEventArgs args)
{
if (args.LeftButton == MouseButtonState.Pressed)
{
IsAutoScrolling = false;
Debug.Print("Mouse Down...");
}
}
/// <summary>
/// 滚动栏触发
/// </summary>
private void ScrollLoadedTrigger(RoutedEventArgs args)
{
if (args.Source is ScrollViewer scrollView )
{
_scrollViewer = scrollView;
Debug.Print("Scroll loaded...");
}
}
/// <summary>
/// Markdown控件对象更新触发
/// </summary>
private void MarkdownOBJTrigger(object obj)
{
if (_markdownViewer != null) return;
if (obj is MarkdownViewer markdownViewer)
{
_markdownViewer = markdownViewer;
_markdownViewer.Markdown = "";
}
}
#endregion
#region 其他方法
/// <summary>
/// 输出文本
/// </summary>
public void AppendText(string newText)
{
Debug.Print(newText);
_markdownViewer.Markdown += newText;
_message.Append(newText);
}
/// <summary>
/// 提交校验
/// </summary>
private bool SubmintChecked(string input)
{
if (string.IsNullOrEmpty(input)) return false;
if (input.Length<2) return false;
if (input.Equals("停止")) return false;
return true;
}
#endregion
#region 回调方法
/// <summary>
/// 加载聊天记录回调
/// </summary>
public void LoadChatRecordCallback(string path)
{
Debug.Print(path);
_scrollViewer.ScrollToTop();
_markdownViewer.Markdown = _fileIO. ReadTxt(path);
}
#endregion
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
Views
UserChatView
<UserControl x:Class="OfflineAI.Views.UserChatView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:OfflineAI.Views"
xmlns:markdig ="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!--绑定数据上下文-->
<UserControl.DataContext>
<viewmodels:UserChatViewModel x:Name="UserWindow"/>
</UserControl.DataContext>
<Grid>
<!--命令绑定事件:窗体加载时传参数Markdown控件对象。在Grid中创建,否则会出现null异常-->
<behavior:Interaction.Triggers>
<behavior:EventTrigger EventName="Loaded">
<behavior:InvokeCommandAction
Command="{Binding MarkdownOBJCommand}"
CommandParameter="{Binding ElementName=MarkdownContent}"/>
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
<!--定义行-->
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<!--行背景色-->
<Border Grid.Row="0" Background="#FFFFFF"/>
<Border Grid.Row="1" Background="#5E5E5E"/>
<Grid>
<!--markdown 滑动条-->
<ScrollViewer Background="#AEAEAE"
x:Name="MarkDownScrollViewer">
<behavior:Interaction.Triggers>
<behavior:EventTrigger EventName="Loaded">
<behavior:InvokeCommandAction
Command="{Binding ScrollLoadedCommand}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
<!--markdown-->
<markdig:MarkdownViewer
Name="MarkdownContent">
<!--命令绑定事件:鼠标滚动显示内容-->
<behavior:Interaction.Triggers>
<!--鼠标滚动命令事件-->
<behavior:EventTrigger EventName="PreviewMouseWheel">
<behavior:InvokeCommandAction
Command="{Binding MouseWheelCommand}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
<!--鼠标点击命令事件-->
<behavior:EventTrigger EventName="PreviewMouseDown">
<behavior:InvokeCommandAction
Command="{Binding MouseDownCommand}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
</markdig:MarkdownViewer>
</ScrollViewer>
</Grid>
<!--第三行内容:显示回话内容-->
<Grid Grid.Row="1" Margin="2">
<!--定义三行-->
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="*"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<!--设置Border样式-->
<Border Grid.Row="0" Margin="150,0,150,0" Background="#5E5E5E">
<Border.BorderThickness>2,2,2,0</Border.BorderThickness>
<Border.BorderBrush>
<SolidColorBrush Color="#000000"/>
</Border.BorderBrush>
</Border>
<Border Grid.Row="1" Margin="150,0,150,0" Background="#5E5E5E">
<Border.BorderThickness>2,0,2,0</Border.BorderThickness>
<Border.BorderBrush>
<SolidColorBrush Color="#000000"/>
</Border.BorderBrush>
</Border>
<Border Grid.Row="2" Margin="150,0,150,0" Background="#5E5E5E">
<Border.BorderThickness>2,0,2,2</Border.BorderThickness>
<Border.BorderBrush>
<SolidColorBrush Color="#000000"/>
</Border.BorderBrush>
</Border>
<!--第2行内容区域-->
<Grid Grid.Row="1" Margin="150,0,150,0">
<TextBox x:Name="InputBox" Background="#5E5E5E"
Text="{Binding CurrentInputText , Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Grid.Row="1" Margin="5" AcceptsReturn="True"
VerticalScrollBarVisibility="Auto">
<!--回车发送-->
<TextBox.InputBindings>
<KeyBinding Command="{Binding SubmiQuestionCommand}" Key="Enter"/>
</TextBox.InputBindings>
</TextBox>
</Grid>
<!--第3行内容区域-->
<Grid Grid.Row="2" Margin="150,0,150,0">
<WrapPanel HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,5,0">
<Button Width="50" Command="{Binding LoadFileCommand}">
<Image Width="24" Height="24"
Source="/Views/Resources/append24-black.png"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Button>
<Button Width="50" Command="{Binding SubmiQuestionCommand}" Content="{Binding SubmitButtonName}"></Button>
</WrapPanel>
</Grid>
</Grid>
</Grid>
</UserControl>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
SystemSettingView
<UserControl x:Class="OfflineAI.Views.SystemSettingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OfflineAI.Views"
xmlns:viewModels="clr-namespace:OfflineAI.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid>
<StackPanel Background="#FFFFFF" Margin="5">
<TextBox FontSize="36" IsReadOnly="True"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center">系统设置</TextBox>
<CheckBox Width="200" Margin="5" HorizontalAlignment="Left" IsChecked="True">是否滚动显示</CheckBox>
<ComboBox Width="200" Margin="5" HorizontalAlignment="Left">
</ComboBox>
</StackPanel>
</Grid>
</UserControl>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
Styles \ ButtonStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 定义圆角按钮的静态样式 -->
<Style x:Key="RoundCornerButtonStyle" TargetType="Button">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#04D3F2" Offset="0.6" />
<GradientStop Color="#FFAB0D" Offset="2.8" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="DarkGray"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Width" Value="60"/>
<Setter Property="Height" Value="20"/>
<!--设置模板样式-->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<!--使用 Border 元素作为按钮的主要容器。
roundedRectangle:名称,方便在触发器中引用。
Background:绑定背景色到按钮的 Background 属性。
BorderBrush:绑定边框颜色到按钮的 BorderBrush 属性。
BorderThickness:绑定边框宽度到按钮的 BorderThickness 属性。
CornerRadius:设置边框的圆角半径为10,使按钮具有圆角效果。
ContentPresenter:用于显示按钮的内容(如文本或图标)。
-->
<Border x:Name="roundedRectangle" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
<!-- 设置顶部圆角 -->
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<!-- 鼠标悬停时 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="roundedRectangle" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#FFB3B3" Offset="0.4" />
<GradientStop Color="#D68B8B" Offset="0.7" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<!-- 按钮被按下时 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="roundedRectangle" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#D68B8B" Offset="0.4" />
<GradientStop Color="#A05252" Offset="0.7" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 定义带图标的按钮的静态样式 -->
<Style x:Key="IconButtonStyle" TargetType="Button">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#AED3D2" Offset="0.3" />
<!-- 淡色 -->
<GradientStop Color="#F0FBFF" Offset="0.7" />
<!-- 深色 -->
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="DarkGray"></Setter>
<Setter Property="BorderThickness" Value="0"></Setter>
<Setter Property="Padding" Value="5"></Setter>
<Setter Property="Margin" Value="5 5 5 5"></Setter>
<Setter Property="FontSize" Value="20"></Setter>
<!-- 调整宽度以适应图标和文本 -->
<Setter Property="Height" Value="50"></Setter>
<!-- 调整高度以适应图标和文本 -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="roundedRectangle" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
<!-- 使用 StackPanel 来布局图标和文本 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<ContentPresenter Content="{TemplateBinding Content}" />
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<!-- 鼠标悬停时 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="roundedRectangle" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#FFB3B3" Offset="0.4" />
<GradientStop Color="#D68B8B" Offset="0.7" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<!-- 按钮被按下时 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="roundedRectangle" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#D68B8B" Offset="0.4" />
<GradientStop Color="#A05252" Offset="0.7" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
MainWindow
<Window x:Class="OfflineAI.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:behavior="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:OfflineAI"
xmlns:viewmodels="clr-namespace:OfflineAI.ViewModels"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d"
Title="ChatAI" Height="800" Width="1000"
Icon="/Views/Resources/app-logo128.ico"
MinHeight="600" MinWidth="800">
<!--绑定上下文-->
<Window.DataContext>
<viewmodels:MainViewModel>
</viewmodels:MainViewModel>
</Window.DataContext>
<!--样式资源-->
<Window.Resources>
<ResourceDictionary>
<!--资源字典: 添加控件样式-->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Views/Styles/ButtonStyle.xaml"/>
<ResourceDictionary Source="Views/Styles/ComboBoxStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<!--事件命令绑定-->
<behavior:Interaction.Triggers>
<!--窗体加载命令绑定-->
<behavior:EventTrigger EventName="Loaded">
<behavior:InvokeCommandAction Command="{Binding LoadedWindowCommand}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
<!--窗体关闭命令绑定-->
<behavior:EventTrigger EventName="Closing">
<behavior:InvokeCommandAction Command="{Binding ClosingWindowCommand}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
<Grid>
<!-- 定义3列:-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<!-- 定义2行 -->
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<!-- 折叠栏 Expander -->
<Expander x:Name="expanderBox" Grid.Row="0" Grid.Column="0" Header=""
Background="#AABBBB" ExpandDirection="Left"
IsExpanded="False"
FlowDirection="LeftToRight" Width="{Binding ExpandedBarWidth}">
<!--命令绑定事件-->
<behavior:Interaction.Triggers>
<!--折叠栏展开命令绑定-->
<behavior:EventTrigger EventName="Expanded">
<behavior:InvokeCommandAction Command="{Binding ExpandedMenuCommand}" />
</behavior:EventTrigger>
<!--折叠栏折叠命令绑定-->
<behavior:EventTrigger EventName="Collapsed">
<behavior:InvokeCommandAction Command="{Binding CollapsedMenuCommand}" />
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
<ScrollViewer Background="#AEAEAE" x:Name="RecordScrollViewer">
<ListBox ItemsSource="{Binding ChatRecordCollection}" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- 显示消息内容 -->
<TextBlock Text="{Binding Data}" Margin="10,0,0,0">
<behavior:Interaction.Triggers>
<!--鼠标点击命令事件-->
<behavior:EventTrigger EventName="PreviewMouseDown">
<behavior:InvokeCommandAction
Command="{Binding DataContext.ChatRecordMouseDownCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
PassEventArgsToCommand="True"/>
</behavior:EventTrigger>
</behavior:Interaction.Triggers>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Expander>
<!-- 右侧内容区域 -->
<Border Background="LightGray" Grid.Row="0" Grid.Column="1" Padding="10"/>
<!--主要区域-->
<Grid Grid.Row="0" Grid.Column="1" Margin="3">
<!--定义三行-->
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
<RowDefinition Height="350"/>
</Grid.RowDefinitions>
<!--设置背景色-->
<Border Grid.Row="0" Background="#99BBCC"/>
<Border Grid.Row="1" Background="#FFFFFF" Grid.RowSpan="2"/>
<!--第一行内容:左对齐内容-->
<WrapPanel VerticalAlignment="Center">
<!--视图切换:首页-->
<Button x:Name="Btn_HomePage" Width="50" Height="36" FontSize="16"
Style="{StaticResource IconButtonStyle}"
Command="{Binding SwitchViewCommand}"
CommandParameter="UserChatView">
<StackPanel Orientation="Horizontal">
<Image Source="Views/Resources/home24-black.png"
Margin="5" Width="24" Height="24"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!--视图切换:新聊天界面-->
<Button x:Name="Btn_Chat" Width="100" Height="36" FontSize="16"
Style="{StaticResource IconButtonStyle}"
Command="{Binding SwitchViewCommand}"
CommandParameter="NewUserChatView">
<StackPanel Orientation="Horizontal">
<Image Source="Views/Resources/edit24-black.png"
Margin="5" Width="24" Height="24"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="新聊天" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!--模型列表-->
<Label Content="模型:" Margin="5" FontSize="18" VerticalAlignment="Center"/>
<ComboBox x:Name="Cbx_ModelList"
Style="{StaticResource RoundComboBoxStyle}"
ItemsSource="{Binding ModelListCollection}"
SelectedItem="{Binding SelectedModel}">
</ComboBox>
</WrapPanel>
<!--第一行内容:右对齐内容-->
<WrapPanel Margin="0,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" >
<Button Background="#99BBCC"
Command="{Binding SwitchViewCommand}"
CommandParameter="SystemSettingView">
<Image Source="/Views/Resources/setting64.png"
Margin="5" Width="24" Height="24"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Button>
</WrapPanel>
<!--第二行内容:显示当前视图-->
<ContentControl Grid.Row="1" Margin="5,5,5,5"
Content="{Binding CurrentView}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" Grid.RowSpan="2"/>
</Grid>
</Grid>
</Window>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
结语
- 以上为项目的全部代码,下面是最后总结。
实现功能:

最后
- 也可以关注微信公众号 [编程笔记in] 社区,共同学习交流!
- 项目源码: https://github.com/timenodes/OfflineAI
1977

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



