前言
就是想做一个摸鱼的小工具。。。
后续可能会优化(应该)。。
所有代码复制即用即可,如果嫌麻烦,也可以直接去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="<< 上一章" Grid.Column="0" Click="BtnPrevChapter_Click" HorizontalAlignment="Left" Margin="2"/>
<Button x:Name="BtnBottomPrevPage" Content="< 上一页" 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();
}
}