从0到1开发ToastFish:WPF框架下的MVVM架构实践
【免费下载链接】ToastFish 一个利用摸鱼时间背单词的软件。 项目地址: https://gitcode.com/GitHub_Trending/to/ToastFish
引言:摸鱼背单词的痛点与解决方案
你是否也曾在工作间隙想背单词却找不到合适的工具?传统背单词软件要么过于复杂影响工作,要么功能单一无法有效记忆。ToastFish作为一款专为摸鱼时间设计的背单词软件,通过WPF框架与MVVM架构的结合,实现了轻量高效的单词学习体验。本文将带你深入剖析其架构设计与实现细节,让你掌握如何从零构建一个兼具实用性与技术深度的桌面应用。
读完本文你将获得:
- WPF中MVVM架构的最佳实践
- SM2记忆算法的工程化实现
- SQLite数据库在桌面应用中的高效应用
- 桌面通知与热键系统的整合方案
- 模块化设计与松耦合代码组织技巧
技术栈选型与架构概览
核心技术栈
| 技术/框架 | 版本 | 用途 |
|---|---|---|
| WPF | .NET Framework 4.8 | 桌面应用UI框架 |
| MVVM Light | 5.4.1 | MVVM模式实现 |
| SQLite | 3.36.0 | 本地数据存储 |
| NAudio | 1.10.0 | 音频播放 |
| NPOI | 2.5.3 | Excel文件处理 |
MVVM架构总览
ToastFish严格遵循MVVM架构,将UI逻辑与业务逻辑分离:
- View:XAML文件定义界面,通过DataContext绑定ViewModel
- ViewModel:封装UI逻辑与数据,通过ICommand与Model交互
- Model:包含核心业务逻辑(SM2算法、单词推送、数据库操作)
- ViewModelLocator:实现依赖注入,解耦ViewModel与View
项目结构深度解析
ToastFish/
├── App.xaml # 应用入口,资源配置
├── View/
│ └── ToastFish.xaml # 主界面
├── ViewModel/
│ ├── ToastFishModel.cs # 主ViewModel
│ └── ViewModelLocator.cs # 依赖注入
├── Model/
│ ├── SM2plus/ # SM2算法实现
│ │ ├── Card.cs # 单词卡片模型
│ │ └── Parameters.cs # 算法参数
│ ├── PushControl/ # 推送控制
│ │ ├── PushWords.cs # 单词推送逻辑
│ │ └── WordType.cs # 单词类型定义
│ ├── SqliteControl/ # 数据库操作
│ │ └── Select.cs # 查询操作
│ └── Mp3/ # 音频播放
└── Resources/ # 静态资源
├── inami.db # SQLite数据库
└── 语音文件/ # 单词发音音频
核心模块实现详解
1. ViewModel与依赖注入
ViewModelLocator实现:
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<ToastFishModel>();
}
public ToastFishModel Main => ServiceLocator.Current.GetInstance<ToastFishModel>();
}
XAML中绑定ViewModel:
<Window DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
<Button Command="{Binding Push}" Content="开始背单词"/>
</Window>
通过SimpleIoc实现的依赖注入,使得View与ViewModel完全解耦,便于单元测试与代码维护。
2. SM2记忆算法实现
SM2算法(SuperMemo 2)是ToastFish的核心,用于优化单词复习间隔。其实现位于Model/SM2plus/Card.cs:
public void updateCard(double curScore)
{
switch (status)
{
case Cardstatus.New:
if (curScore == Parameters.Easy)
{
status = Cardstatus.Reviewed;
dateLastReviewed = DateTime.Now;
daysBetweenReviews = 1; // 首次Easy间隔1天
}
// 其他评分情况处理...
}
// 难度系数更新公式
difficulty += podue * (8 - 10 * curScore) / 17;
difficulty = Math.Clamp(difficulty, 0, 1);
// 间隔更新公式
daysBetweenReviews *= (1 + (3 - 1.7 * difficulty) * podue * (0.95 + 0.1 * rnd.NextDouble()));
}
算法核心参数定义在Parameters.cs:
public static class Parameters
{
public const double Again = 0.4; // 错误
public const double Hard = 0.6; // 困难
public const double Good = 0.8; // 良好
public const double Easy = 1; // 简单
public const double delayAgain = 1; // 再次学习延迟(分钟)
// 其他参数...
}
3. 单词推送系统设计
单词推送是ToastFish的核心功能,通过Windows通知实现:
public void pushCard(Card card)
{
// 构建Toast通知
new ToastContentBuilder()
.AddText(card.word.headWord) // 单词
.AddText(card.word.tranCN) // 翻译
.AddButton(new ToastButton()
.SetContent("困难")
.AddArgument("action", "hard")) // 困难按钮
// 其他按钮...
.Show();
// 等待用户反馈
var score = ProcessToastNotificationRecitationSM2().Result;
card.updateCard(score); // 更新卡片状态
UpdateDatabase(card); // 持久化到数据库
}
热键支持实现:
new HotKey(Key.Q, KeyModifier.Alt, OnHotKeyHandler); // ALT+Q开始学习
new HotKey(Key.D1, KeyModifier.Alt, OnHotKeyHandler); // ALT+1标记困难
private void OnHotKeyHandler(HotKey hotKey)
{
switch (hotKey.Key.ToString())
{
case "Q": Begin_Click(null, null); break;
case "D1": PushWords.HotKeytObservable.raiseEvent("1"); break;
// 其他热键处理...
}
}
4. 数据持久化方案
使用SQLite实现单词数据的持久化:
public void updateCardDateBase(List<Card> cards)
{
foreach (var card in cards)
{
var sql = $"UPDATE {TABLE_NAME} SET
status = {(int)card.status},
difficulty = {card.difficulty},
daysBetweenReviews = {card.daysBetweenReviews},
lastScore = {card.lastScore},
dateLastReviewed = '{card.dateLastReviewed}'
WHERE wordRank = {card.word.wordRank}";
ExecuteNonQuery(sql);
}
}
数据库表结构设计:
CREATE TABLE CET4_1 (
wordRank INTEGER PRIMARY KEY,
headWord TEXT,
tranCN TEXT,
difficulty REAL DEFAULT 0.3,
daysBetweenReviews REAL DEFAULT 3,
lastScore REAL DEFAULT 0,
dateLastReviewed TEXT,
status INTEGER DEFAULT 0
);
5. 多线程架构设计
为避免UI阻塞,采用多线程设计:
// 启动背单词线程
thread = new Thread(new ParameterizedThreadStart(PushWords.RecitationSM2));
thread.Start(words);
// 线程安全的通知事件
private static readonly object _lock = new object();
private void RaisePropertyChanged(string propertyName)
{
lock (_lock)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
关键技术难点与解决方案
1. MVVM模式下的命令绑定
问题:View如何与ViewModel通信?
解决方案:使用RelayCommand实现命令绑定
// ViewModel中定义命令
public ICommand Push { get; set; }
public ToastFishModel()
{
Push = new RelayCommand(PushTest);
}
private void PushTest()
{
PushWords.RecitationSM2(173); // 调用Model层方法
}
// XAML中绑定命令
<Button Command="{Binding Push}" Content="开始背单词"/>
2. 跨线程UI更新
问题:后台线程如何更新UI?
解决方案:使用Dispatcher
// 在ViewModel中
Application.Current.Dispatcher.Invoke(() =>
{
CurrentWord = newWord;
RaisePropertyChanged("CurrentWord");
});
3. 音频播放与通知整合
问题:如何实现单词发音与通知同步?
解决方案:封装音频播放服务
public bool PlayMp3(List<string> words)
{
try
{
var player = new SoundPlayer($"Resources/Goin/{words[0]}.mp3");
player.PlaySync();
return true;
}
catch
{
// fallback到系统语音合成
var synth = new SpeechSynthesizer();
synth.SpeakAsync(words[0]);
return false;
}
}
性能优化策略
1. 数据库查询优化
// 优化前
foreach (var word in wordList)
{
ExecuteQuery($"SELECT * FROM words WHERE id={word.id}");
}
//优化后
var ids = wordList.Select(w => w.id).ToList();
ExecuteQuery($"SELECT * FROM words WHERE id IN ({string.Join(',', ids)})");
2. 内存管理
// 使用using释放资源
using (var connection = new SQLiteConnection(connectionString))
{
connection.Open();
// 数据库操作
}
// 图片资源延迟加载
public ImageSource WordImage
{
get
{
if (_imageSource == null)
{
_imageSource = LoadImageAsync(_imagePath);
}
return _imageSource;
}
}
测试与调试技巧
1. ViewModel单元测试
[TestClass]
public class ToastFishModelTest
{
[TestMethod]
public void PushCommandTest()
{
// Arrange
var locator = new ViewModelLocator();
var vm = locator.Main;
// Act
vm.Push.Execute(null);
// Assert
Assert.IsTrue(vm.IsReciting);
}
}
2. 调试SM2算法
// 添加调试日志
Debug.WriteLine($"Card {card.word.headWord} 更新前: " +
$"difficulty={card.difficulty:0.00}, " +
$"interval={card.daysBetweenReviews:0.00}");
card.updateCard(Parameters.Good);
Debug.WriteLine($"更新后: " +
$"difficulty={card.difficulty:0.00}, " +
$"interval={card.daysBetweenReviews:0.00}");
部署与发布
1. 应用打包
<!-- ToastFish.csproj -->
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<ApplicationIcon>chika64.ico</ApplicationIcon>
</PropertyGroup>
2. 数据库文件部署
// 处理数据库路径
string databasePath = Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"Resources", "inami.db");
总结与展望
ToastFish通过WPF与MVVM架构的结合,成功实现了一个高效、轻量的背单词工具。核心亮点包括:
- 架构设计:严格遵循MVVM模式,实现关注点分离
- 算法实现:工程化SM2记忆算法,优化复习间隔
- 用户体验:Windows通知+热键支持,实现无缝摸鱼背单词
- 数据处理:SQLite本地存储,确保学习数据不丢失
未来优化方向
- 跨平台迁移:使用MAUI替代WPF,实现Windows/macOS/Linux支持
- 云同步:添加OneDrive/Google Drive同步功能
- AI助手:集成GPT生成单词例句与记忆法
- 学习数据分析:可视化学习曲线与记忆效率
通过本文的讲解,相信你已经掌握了MVVM架构在实际项目中的应用,以及如何将复杂算法工程化实现。无论是开发教育类软件还是其他桌面应用,这些经验都将助你构建更高质量的软件产品。
附录:核心代码清单
1. SM2算法实现(Card.cs)
public void updateCard(double curScore)
{
bool isLapsed = false;
switch (status)
{
case Cardstatus.New:
if (curScore == Parameters.Easy)
{
status = Cardstatus.Reviewed;
dateLastReviewed = DateTime.Now;
}
// 其他状态处理...
}
// 计算逾期率
double podue = Math.Min(2, daysSpan.TotalDays / daysBetweenReviews);
// 更新难度
difficulty += podue * (8 - 10 * curScore) / 17;
difficulty = Math.Clamp(difficulty, 0, 1);
// 更新间隔
double dfweight = 3 - 1.7 * difficulty;
daysBetweenReviews *= (1 + dfweight * podue * (0.95 + 0.1 * rnd.NextDouble()));
}
2. ViewModel实现(ToastFishModel.cs)
public class ToastFishModel : ViewModelBase
{
private string _currentWord;
public string CurrentWord
{
get => _currentWord;
set { _currentWord = value; RaisePropertyChanged(); }
}
public ICommand PushCommand { get; }
public ToastFishModel()
{
PushCommand = new RelayCommand(StartRecitation);
}
private void StartRecitation()
{
Task.Run(() => PushWords.RecitationSM2(Select.WORD_NUMBER));
}
}
3. 数据库操作(Select.cs)
public List<Card> GetOverdueCards(int count)
{
var cmd = $"SELECT * FROM {TABLE_NAME} WHERE " +
$"dateLastReviewed < '{DateTime.Now.AddDays(-1):yyyy-MM-dd}' " +
$"ORDER BY difficulty DESC LIMIT {count}";
return DataBase.Query<Word>(cmd).Select(w => new Card(w)).ToList();
}
希望本文能为你的WPF/MVVM项目提供有价值的参考。如有任何问题或建议,欢迎在评论区留言讨论。Happy coding!
如果你觉得本文有帮助,请点赞、收藏、关注三连,下期将带来《ToastFish性能优化实战》
【免费下载链接】ToastFish 一个利用摸鱼时间背单词的软件。 项目地址: https://gitcode.com/GitHub_Trending/to/ToastFish
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



