C# 实现一个小说阅读器 WPF 附源码

C# 实现一个小说阅读器 WPF 附源码

前言

就是想做一个摸鱼的小工具。。。
后续可能会优化(应该)。。

所有代码复制即用即可,如果嫌麻烦,也可以直接去git下载。
https://github.com/HelloPte/NovelReader/tree/main

效果图

在这里插入图片描述

界面

<Window x:Class="NovelReader.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:NovelReader"
        mc:Ignorable="d"
        Title="Novel Reader" Height="600" Width="800">
    <Window.Resources>
        <!-- 覆写系统选中/高亮颜色 -->
        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="WhiteSmoke"/>
        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="ForestGreen"/>
        
        <Style TargetType="TreeViewItem">
            <Setter Property="Foreground" Value="Black" />
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="Padding" Value="2" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background" Value="LightYellow" />
                    <Setter Property="Foreground" Value="ForestGreen" />
                </Trigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#FFBEE6FD" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200" x:Name="Dircolum" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <!-- 目录 -->
        <TreeView x:Name="ChapterTree" SelectedItemChanged="ChapterTree_SelectedItemChanged" />
        <!-- 阅读页 -->
        <DockPanel Grid.Column="1">
            <!-- 顶部按钮 -->
            <DockPanel  DockPanel.Dock="Top" HorizontalAlignment="Right" Margin="5,5,20,5">
                <Button x:Name="BtnToggleDirectory" Content="隐藏目录" Click="BtnToggleDirectory_Click" HorizontalAlignment="Left"/>
                <Button x:Name="BtnSelectBook" Content="选择图书" Click="BtnSelectBook_Click" Width="67"  HorizontalAlignment="Right"/>
            </DockPanel>
            <!-- 底部翻页区 -->
            <Grid DockPanel.Dock="Bottom" Margin="5,5,20,5">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <Button x:Name="BtnBottomPrevChapter" Content="&lt;&lt; 上一章" Grid.Column="0" Click="BtnPrevChapter_Click" HorizontalAlignment="Left" Margin="2"/>
                <Button x:Name="BtnBottomPrevPage" Content="&lt; 上一页" Grid.Column="2" Click="BtnPrevPage_Click" HorizontalAlignment="Right" Margin="2"/>
                <Button x:Name="BtnBottomNextPage" Content="下一页 >" Grid.Column="3" Click="BtnNextPage_Click" HorizontalAlignment="Left" Margin="2"/>
                <Button x:Name="BtnBottomNextChapter" Content="下一章 >>" Grid.Column="5" Click="BtnNextChapter_Click" HorizontalAlignment="Right" Margin="2"/>
            </Grid>
            <!-- 文本显示 -->
            <ScrollViewer x:Name="ReadScroller" VerticalScrollBarVisibility="Auto">
                <TextBlock x:Name="TxtDisplay" TextWrapping="Wrap" FontSize="16" Padding="10" />
            </ScrollViewer>
        </DockPanel>

    </Grid>
</Window>

DTO

/// <summary>
 ///  章节
 /// </summary>
 public class Chapter
 {
     public string Title;  // 标题
     public string Text;
 }

 /// <summary>
 /// 阅读进度
 /// </summary>
 public class ReadingState
 {
     public int Chapter; // 章节
     public int Page;  // 页数
 }

 /// <summary>
 /// 图书进度
 /// </summary>
 public class BookState
 {
     public string Path;
     public int Chapter;
     public int Page;
 }

主窗体代码

public partial class MainWindow : Window
{
    /// <summary>
    /// 章节列表
    /// </summary>
    private List<Chapter> Chapters = new List<Chapter>();
    /// <summary>
    /// 当前章节索引
    /// </summary>
    private int currentChapterIndex;
    /// <summary>
    /// 当前页数索引
    /// </summary>
    private int currentPageIndex;
    /// <summary>
    /// 每页字数
    /// </summary>
    private const int CharsPerPage = 1000;
    /// <summary>
    /// 当前文件路径
    /// </summary>
    private string currentFilePath;
    /// <summary>
    /// 阅读进度字典
    /// </summary>
    private Dictionary<string, ReadingState> stateDict = new Dictionary<string, ReadingState>();
    /// <summary>
    /// 阅读进度存储文件
    /// </summary>
    private const string StateFile = "NovelReadingStates.xml";

    public MainWindow()
    {
        InitializeComponent();
        LoadReadStates();
    }

    #region 导航按钮
    // 上一章
    private void BtnPrevChapter_Click(object sender, RoutedEventArgs e)
    {
        if (currentChapterIndex > 0)
        {
            currentChapterIndex--;
            currentPageIndex = 0;
            DisplayPage();
        }
    }

    // 上一页
    private void BtnPrevPage_Click(object sender, RoutedEventArgs e)
    {
        if (currentPageIndex > 0)
        {
            currentPageIndex--;
        }
        else if (currentChapterIndex > 0)
        {
            // 本章第一页,跳到上一章最后一页
            currentChapterIndex--;
            var prevChapterText = Chapters[currentChapterIndex].Text;
            currentPageIndex = (int)Math.Ceiling((double)prevChapterText.Length / CharsPerPage) - 1;
        }
        // 已经是全书第一页
        DisplayPage();
    }

    // 下一页
    private void BtnNextPage_Click(object sender, RoutedEventArgs e)
    {
        var chapterText = Chapters[currentChapterIndex].Text;
        int totalPages = (int)Math.Ceiling((double)chapterText.Length / CharsPerPage);

        if (currentPageIndex < totalPages - 1)
        {
            currentPageIndex++;
        }
        else if (currentChapterIndex < Chapters.Count - 1)
        {
            // 本章已是最后一页,跳到下一章第一页
            currentChapterIndex++;
            currentPageIndex = 0;
        }
        // 已经是全书最后一页
        DisplayPage();
    }

    // 下一章
    private void BtnNextChapter_Click(object sender, RoutedEventArgs e)
    {
        if (currentChapterIndex < Chapters.Count - 1)
        {
            currentChapterIndex++;
            currentPageIndex = 0;
            DisplayPage();
        }
    }

    // 选择图书
    private void BtnSelectBook_Click(object sender, RoutedEventArgs e)
    {
        var dlg = new OpenFileDialog { Filter = "文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*" };
        dlg.Title = "选择图书";
        if (dlg.ShowDialog() != true) return;

        currentFilePath = dlg.FileName;
        LoadChapters(currentFilePath);
        BuildDirectory();
        RestoreReadingState();
        SelectChapterInTree();
        DisplayPage();
    }

    // 隐藏导航栏
    private void BtnToggleDirectory_Click(object sender, RoutedEventArgs e)
    {
        if (ChapterTree.Visibility == Visibility.Visible)
        {
            ChapterTree.Visibility = Visibility.Collapsed;
            Dircolum.Width = new GridLength(0);
            BtnToggleDirectory.Content = "显示目录";
        }
        else
        {
            ChapterTree.Visibility = Visibility.Visible;
            Dircolum.Width = new GridLength(200);
            BtnToggleDirectory.Content = "隐藏目录";
        }
    }
    #endregion

    /// <summary>
    /// 章节切换事件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void ChapterTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (ChapterTree.SelectedItem is TreeViewItem item)
        {
            currentChapterIndex = (int)item.Tag;
            currentPageIndex = 0;
            DisplayPage();
        }
    }

    private void SelectChapterInTree()
    {
        if (currentChapterIndex > 0 && currentChapterIndex < ChapterTree.Items.Count)
        {
            var item = (TreeViewItem)ChapterTree.Items[currentChapterIndex];
            item.IsSelected = true;
            item.BringIntoView();
        }
    }

    /// <summary>
    /// 加载章节
    /// </summary>
    /// <param name="path"></param>
    private void LoadChapters(string path)
    {
        Chapters.Clear();
        string text = null;
        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        using (var sr = new StreamReader(fs, Encoding.UTF8, true))
        {
            text = sr.ReadToEnd();
        }

        // 读取整个文件并查找章节标题
        var matches = Regex.Matches(text,
            //@"^\s*第\d+章\s+(.+)$",
            @"^\s*(?:第\d+章\s+.+|☆、.+)$",
            RegexOptions.Multiline
        );

        if (matches.Count == 0)
        {
            var encodArr = new[] { "gb2312", "gbk" };
            foreach (var name in encodArr)
            {
                Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // 注册系统编码
                var encoding = Encoding.GetEncoding(name);
                text = File.ReadAllText(path, encoding);
                matches = Regex.Matches(text,
                    //@"^\s*第\d+章\s+(.+)$",
                    @"^\s*(?:第\d+章\s+.+|☆、.+)$",
                    RegexOptions.Multiline
                );
                if (matches.Count > 0) break;
            }
        }


        for (int i = 0; i < matches.Count; i++)
        {
            int start = matches[i].Index;
            int end = (i + 1 < matches.Count) ? matches[i + 1].Index : text.Length;
            string chapterText = text.Substring(start, end - start);
            string title = matches[i].Value.Trim();
            Chapters.Add(new Chapter { Title = title, Text = chapterText });
        }
    }

    /// <summary>
    /// 创建目录
    /// </summary>
    private void BuildDirectory()
    {
        ChapterTree.Items.Clear();
        for (int i = 0; i < Chapters.Count; i++)
        {
            var item = new TreeViewItem { Header = Chapters[i].Title, Tag = i };
            ChapterTree.Items.Add(item);
        }
    }


    /// <summary>
    /// 显示页
    /// </summary>
    private void DisplayPage()
    {
        if (Chapters.Count == 0) return;
        var txt = Chapters[currentChapterIndex].Text;
        int start = currentPageIndex * CharsPerPage;
        if (start >= txt.Length) { currentPageIndex = 0; start = 0; }
        int len = Math.Min(CharsPerPage, txt.Length - start);
        TxtDisplay.Text = txt.Substring(start, len);
        SaveCurrentState();
    }

    /// <summary>
    /// 保存阅读进度
    /// </summary>
    private void SaveAllStates()
    {
        var list = new List<BookState>();
        foreach (var kv in stateDict)
            list.Add(new BookState { Path = kv.Key, Chapter = kv.Value.Chapter, Page = kv.Value.Page });

        var xs = new XmlSerializer(typeof(List<BookState>));
        using var fs = new FileStream(StateFile, FileMode.Create, FileAccess.Write);
        xs.Serialize(fs, list);
    }

    /// <summary>
    /// 加载阅读进度
    /// </summary>
    private void LoadReadStates()
    {
        try
        {
            var xs = new XmlSerializer(typeof(List<BookState>));
            using var fs = new FileStream(StateFile, FileMode.Open, FileAccess.Read);
            var list = (List<BookState>)xs.Deserialize(fs);
            stateDict = new Dictionary<string, ReadingState>();
            foreach (var bs in list)
                stateDict[bs.Path] = new ReadingState { Chapter = bs.Chapter, Page = bs.Page };
        }
        catch
        {
            stateDict = new Dictionary<string, ReadingState>();
        }
    }

    /// <summary>
    /// 返回指定章节的总页数
    /// </summary>
    /// <param name="idx"></param>
    /// <returns></returns>
    private int GetPageCount(int idx)
    {
        var result = (int)Math.Ceiling((double)Chapters[idx].Text.Length / CharsPerPage);
        return result;
    }

    /// <summary>
    /// 恢复阅读进度
    /// </summary>
    private void RestoreReadingState()
    {
        if (stateDict.TryGetValue(currentFilePath, out var st))
        {

            currentChapterIndex = Math.Min(st.Chapter, Chapters.Count - 1);  // 恢复章节索引,确保不超范围
            int maxPage = GetPageCount(currentChapterIndex) - 1; // 获取当前章节的最大页码,从0开始
            currentPageIndex = Math.Min(Math.Max(st.Page, 0), maxPage); // 恢复页码索引,确保在0, maxPage范围内
        }
        else
        {
            currentChapterIndex = 0;
            currentPageIndex = 0;
        }
    }

    /// <summary>
    ///  保存当前阅读进度
    /// </summary>
    private void SaveCurrentState()
    {
        stateDict[currentFilePath] = new ReadingState
        {
            Chapter = currentChapterIndex,
            Page = currentPageIndex
        };
        SaveAllStates();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值