从0到1开发ToastFish:WPF框架下的MVVM架构实践

从0到1开发ToastFish:WPF框架下的MVVM架构实践

【免费下载链接】ToastFish 一个利用摸鱼时间背单词的软件。 【免费下载链接】ToastFish 项目地址: https://gitcode.com/GitHub_Trending/to/ToastFish

引言:摸鱼背单词的痛点与解决方案

你是否也曾在工作间隙想背单词却找不到合适的工具?传统背单词软件要么过于复杂影响工作,要么功能单一无法有效记忆。ToastFish作为一款专为摸鱼时间设计的背单词软件,通过WPF框架与MVVM架构的结合,实现了轻量高效的单词学习体验。本文将带你深入剖析其架构设计与实现细节,让你掌握如何从零构建一个兼具实用性与技术深度的桌面应用。

读完本文你将获得:

  • WPF中MVVM架构的最佳实践
  • SM2记忆算法的工程化实现
  • SQLite数据库在桌面应用中的高效应用
  • 桌面通知与热键系统的整合方案
  • 模块化设计与松耦合代码组织技巧

技术栈选型与架构概览

核心技术栈

技术/框架版本用途
WPF.NET Framework 4.8桌面应用UI框架
MVVM Light5.4.1MVVM模式实现
SQLite3.36.0本地数据存储
NAudio1.10.0音频播放
NPOI2.5.3Excel文件处理

MVVM架构总览

mermaid

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架构的结合,成功实现了一个高效、轻量的背单词工具。核心亮点包括:

  1. 架构设计:严格遵循MVVM模式,实现关注点分离
  2. 算法实现:工程化SM2记忆算法,优化复习间隔
  3. 用户体验:Windows通知+热键支持,实现无缝摸鱼背单词
  4. 数据处理:SQLite本地存储,确保学习数据不丢失

未来优化方向

  1. 跨平台迁移:使用MAUI替代WPF,实现Windows/macOS/Linux支持
  2. 云同步:添加OneDrive/Google Drive同步功能
  3. AI助手:集成GPT生成单词例句与记忆法
  4. 学习数据分析:可视化学习曲线与记忆效率

通过本文的讲解,相信你已经掌握了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 一个利用摸鱼时间背单词的软件。 【免费下载链接】ToastFish 项目地址: https://gitcode.com/GitHub_Trending/to/ToastFish

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值