C#入门学习-----图书阅读器(WPF 用户控件技术)

编译平台:VS2008 + .Net Framework 3.5

        语言: C#


1、图书阅读器系统架构
1、2 系统架构设计
在这个系统中出现在的实体有图书目录、图书列表、图书、压缩格式的图书、图像缓存等。

(1) 文件夹可以直接定义为一个类。因为该对象相对固定,不同的文件夹除了名称唾位置不一样外,还可能会有一些其他变化的特性。

(2)每个文件夹包含多部书。因为图书的类型不是固定的,比如有压缩文件类型的图书和其他格式的图书,需要抽象出来实现一个接口。

(3) 每本书包含多个页面。因为每个页面的格式是不同的,因些也需要进行抽象。

(4) 每本图书会包含一个图像缓存,该缓存提供的功能相对固定,当然也可以进一步抽象。

 C#入门学习-----图书阅读器(WPF 用户控件技术)_第1张图片

Catalog代表一个文件夹类,它包含代表该目录下所有图书的ObservableCollection泛型集合类。

IBook是抽象出来的代表一部图书的接口,它实现了INotifyPropertyChanged以便实现UI级别的绑定。

BaseBook是一个实现了IBook接口的类,提供了对于每本图书的基本实现。

RarBook通过派生自BaseBook类,实现了压缩格式的图书对象;

IBookItem接口是代表图书书页的接口,IBook接口包含一个类型为List泛型集合,来表示一本书的所有图书页。

RarPage实现了IBookItem接口,提供了对于RarBook类型图书的书页实现。

 

1、3 项目文件夹介绍

C#入门学习-----图书阅读器(WPF 用户控件技术)_第2张图片
在此图中

Dependencies文件夹包含了项目中使用到的第三方类库或程序,比如pdftohtml.exe用于将pdf文件转换为Html格式。

                          SevenZipSharp.dll用于压缩或解压缩文件,使用的时候需要7z.dll来进行压缩或解压缩。

                         WPFToolkit.dll包含一些额外的控件来丰富WPF控件。

项目根目录下的app.config是应用程序配置文件。

 

2、系统核心类的实现
这一节将介绍如何实现。主要内容涵盖了.NET的反射、多线程、操作文件和文件夹知识,以及如何使用面向对象方式设计和实现类。

2、1  实现图书目录Catalog类
图书阅读器每次在启动时,会根据在选项指定的文件路径异步加载图书到ListBox以显示书籍。

或者用户单击“打开”按钮,从弹出的打开文件窗口中选择一个文件。

Catalog会将该文件加载到其图书列表中,Catalog类要能从文件夹中枚举图书文件,也要能从特定文件中加载图书。

 C#入门学习-----图书阅读器(WPF 用户控件技术)_第3张图片

从图可以看出,Catalog包含实现了IBook接口的实例列表,作为其内含的图书。

因为该类不被设计用于继承或开,因此将该类指定为Internal.

[csharp]
internal class Catalog 

 internal class Catalog

Catalog类定义了3个属性,分别用于指定文件路径、用于保存图书的列表及一个布尔值获取和设定图书变更信息。

图书列表采用泛型集合,原因是因为它采用了集合通知。

[csharp]
#region -----------------属性区域-----------------  
 
        private string _bookPath = string.Empty;//文件路径  
        public string BookPath   //文件路径属性  
        { 
            get { return _bookPath; }//返回值  
            set { _bookPath = value; }//设置值  
        } 
        private ObservableCollection _Books =//图书集合  
            new ObservableCollection(); 
        public ObservableCollection Books  //图书集合属性  
        { 
          get { return _Books; }  //返回图书列表  
          set { _Books = value; } //设置图书列表  
        } 
 
        private bool _IsChanged = false;//是否变更  
        public bool IsChanged  //是否变更属性  
        { 
            get { return _IsChanged; }  //返回变更  
            set { _IsChanged = value; } //设置变更  
        } 
 
        #endregion 

#region -----------------属性区域-----------------

  private string _bookPath = string.Empty;//文件路径
  public string BookPath   //文件路径属性
  {
   get { return _bookPath; }//返回值
   set { _bookPath = value; }//设置值
  }
  private ObservableCollection _Books =//图书集合
            new ObservableCollection();
  public ObservableCollection Books  //图书集合属性
  {
    get { return _Books; }  //返回图书列表
    set { _Books = value; } //设置图书列表
  }

  private bool _IsChanged = false;//是否变更
  public bool IsChanged  //是否变更属性
  {
   get { return _IsChanged; }  //返回变更
   set { _IsChanged = value; } //设置变更
  }

  #endregion
 当在选项中设置好图书的路径后,每次启动程序时,会从app.config中读取设置好的图书路径,再调用重载的确Load()方式从路径中加载图书。

Load有两个重载方法。

一个接受一个文件路径作为参数,该路径将会被赋给Catalog对象的BookPath属性;

另一个Load()方法会根据该属性的值来从目录中加载图书。Load带参数的重载方法实现如下:

[csharp]
public void Load(string path)//该重载方法传入一个文件路径  
 
try 

    _bookPath = path;//将路径指定给BookPath属性  
    Load();          //调用不带参数的重载的Load方法  

catch (Exception err) //在加载过程出现错误则触发异常  
{    //调用定制的异常管理窗口显示异常信息  
    ExceptionManagement.Manage("Catalog:LoadPath", err); 

   public void Load(string path)//该重载方法传入一个文件路径
  {
   try
   {
    _bookPath = path;//将路径指定给BookPath属性
    Load();          //调用不带参数的重载的Load方法
   }
   catch (Exception err) //在加载过程出现错误则触发异常
   {    //调用定制的异常管理窗口显示异常信息
    ExceptionManagement.Manage("Catalog:LoadPath", err);
   }
  }
代码内部调用了Load另一个重载,如果产生异常,则会产生定制的ExceptioMamagerment类的Mange()方法来产生一个异常窗口。

Load() 方法实现了加载的所有核心逻辑。

[csharp]
private void Load()  //不带参数的重载方法实现  
    { 
        try 
        { 
            string bin = System.Reflection.Assembly.//得到所保存的书签文件路径  
                   GetExecutingAssembly().Location.Replace(".exe", ".bin"); 
            if (File.Exists(bin)) //如果存在书签  
            {                    
                if (LoadBooks(bin))//加载书签  
                { 
                    bin = System.Reflection.Assembly. //得到保存的封面文件路径  
                           GetExecutingAssembly().Location.Replace(".exe", ".bin2"); 
                    if (File.Exists(bin))  //如果存在封面  
                    {   //使用一个后台线程异步的加载图书封面                           
            Thread t = new Thread(new ParameterizedThreadStart(LoadCovers)); 
            t.IsBackground = true;//指定线程为后台线程  
            t.Priority = ThreadPriority.BelowNormal;//指定线程优先级较低  
            t.Start(bin); //开始执行线程,并传入Bin参数  
                    } 
                } 
                else  //如果加载书签失败  
                    ParseDirectoryThread();//通过分析文件夹重建书签  
            } 
            else //如果书签文件不存在  
            { 
                   ParseDirectoryThread();//通过分析文件夹重建书签  
            } 
        } 
        catch (Exception err) //产生异常  
        {   //显示一个异常信息窗口,列明异常信息  
            ExceptionManagement.Manage("Catalog:Load", err); 
        } 
    } 

 private void Load()  //不带参数的重载方法实现
  {
   try
   {
    string bin = System.Reflection.Assembly.//得到所保存的书签文件路径
                    GetExecutingAssembly().Location.Replace(".exe", ".bin");
    if (File.Exists(bin)) //如果存在书签
    {     
     if (LoadBooks(bin))//加载书签
     {
      bin = System.Reflection.Assembly. //得到保存的封面文件路径
                            GetExecutingAssembly().Location.Replace(".exe", ".bin2");
      if (File.Exists(bin))  //如果存在封面
      {   //使用一个后台线程异步的加载图书封面       
    Thread t = new Thread(new ParameterizedThreadStart(LoadCovers));
    t.IsBackground = true;//指定线程为后台线程
    t.Priority = ThreadPriority.BelowNormal;//指定线程优先级较低
    t.Start(bin); //开始执行线程,并传入Bin参数
      }
     }
     else  //如果加载书签失败
      ParseDirectoryThread();//通过分析文件夹重建书签
    }
    else //如果书签文件不存在
    {
                    ParseDirectoryThread();//通过分析文件夹重建书签
    }
   }
   catch (Exception err) //产生异常
   {   //显示一个异常信息窗口,列明异常信息
    ExceptionManagement.Manage("Catalog:Load", err);
   }
  }
代码首先使用System.Reflection.Assembly.GetExecutingAssembly返回当前执行的程序集,获取其Location属性的值,

即程序集的位置。

调用Replace()方法将exe扩展名替换为bin扩展名,得到一个与可执行文件相同的.bin文件,这个文件

保存了图书的文件和文件夹信息,该信息作为书签以二进制格式保存,如果该文件存在,则调用LoadBooks()方法加载书签;

另一个与可执行文件具有相同文件名,扩展名为bin2的文件,保存的是每本图书的封面,如果存在,代码使用一个参数线程后台加载图书

封面信息。

 

2、2 加载书签信息
图书阅读器尽量保存用户所读过的书的历史信息,以便下次打开软件时,能直接从前一次的位置开始阅读。

因此在每次关闭软件时,会调用Save()方法来保存这些信息。LoadBooks()方法将从保存的二进制文件中恢复历史记录。

[csharp]
private bool LoadBooks(string fileName)//从文件中加载图书集合信息  
        { 
            bool result = true; //默认结果值  
            IFormatter formatter = new BinaryFormatter();//实例化二进制格式化器  
            Stream stream = new FileStream(fileName,  //创建一个FileStream打开文件流  
                FileMode.Open, 
                FileAccess.Read, 
                FileShare.None); 
            try 
            { 
                //从流中反序列化出文件目录  
                string booksFrom = (string)formatter.Deserialize(stream); 
                //如果图书路径与当前目录位于不同的路径  
                if (this._bookPath != booksFrom || !Directory.Exists(this._bookPath)) 
                {   //新建一个books集合类  
                    this._Books = new ObservableCollection(); 
                    result = false;//加载失败  
                } 
                else //如果是同一个文件夹  
                { 
                    //首先反序列化出图书数目  
                    int count = (int)formatter.Deserialize(stream); 
                    for ( int i = 0; i < count; i++ ) //循环图书数目  
                    { 
                        //反序列化出每个文件的文件路径  
                        string filePath = (string)formatter.Deserialize(stream); 
                        long size = (long)formatter.Deserialize(stream);//文件大小  
                        int nbPages = (int)formatter.Deserialize(stream);//页数  
                        string bookmark = (string)formatter.Deserialize(stream);//书签  
                        bool isread= (bool)formatter.Deserialize(stream);//是否阅读  
                        FileInfo file = new FileInfo( filePath );//获取文件信息  
                        if( file.Exists )//如果文件存在  
                        { 
                            IBook bk = null;//初始化实现IBook接口的对象  
                            //如果书签过滤设置中包含与文件一致的扩展名  
                            if (Properties.Settings.Default.BookFilter. 
                                Contains(file.Extension.ToUpper())) 
                                bk = (IBook)new RarBook(file.FullName, false);//返回一个新的Rar书本对象  
                            bk.Bookmark = bookmark;//指定书签  
                            bk.Size = size;        //指定大小  
                            bk.NbPages = nbPages;  //指定页数  
                            bk.IsRead = isread;    //指定是否阅读  
                            this._Books.Add(bk);   //加载到书签列表  
                        } 
                    } 
                } 
            } 
            catch( Exception err ) //如果出现加载异常  
            {   //在异常处理窗口中显示异常信息  
                ExceptionManagement.Manage("Catalog:LoadBooks", err); 
            } 
            finally 
            { 
                stream.Close(); //文件流使用完成后要关闭以释放非托管资源  
            } 
            return result;      //返回结果  
        } 

private bool LoadBooks(string fileName)//从文件中加载图书集合信息
  {
   bool result = true; //默认结果值
   IFormatter formatter = new BinaryFormatter();//实例化二进制格式化器
   Stream stream = new FileStream(fileName,  //创建一个FileStream打开文件流
    FileMode.Open,
    FileAccess.Read,
    FileShare.None);
   try
   {
    //从流中反序列化出文件目录
    string booksFrom = (string)formatter.Deserialize(stream);
    //如果图书路径与当前目录位于不同的路径
    if (this._bookPath != booksFrom || !Directory.Exists(this._bookPath))
    {   //新建一个books集合类
     this._Books = new ObservableCollection();
     result = false;//加载失败
    }
    else //如果是同一个文件夹
    {
     //首先反序列化出图书数目
     int count = (int)formatter.Deserialize(stream);
     for ( int i = 0; i < count; i++ ) //循环图书数目
     {
      //反序列化出每个文件的文件路径
      string filePath = (string)formatter.Deserialize(stream);
      long size = (long)formatter.Deserialize(stream);//文件大小
      int nbPages = (int)formatter.Deserialize(stream);//页数
      string bookmark = (string)formatter.Deserialize(stream);//书签
                        bool isread= (bool)formatter.Deserialize(stream);//是否阅读
      FileInfo file = new FileInfo( filePath );//获取文件信息
      if( file.Exists )//如果文件存在
      {
       IBook bk = null;//初始化实现IBook接口的对象
                            //如果书签过滤设置中包含与文件一致的扩展名
                            if (Properties.Settings.Default.BookFilter.
                                Contains(file.Extension.ToUpper()))
                                bk = (IBook)new RarBook(file.FullName, false);//返回一个新的Rar书本对象
       bk.Bookmark = bookmark;//指定书签
       bk.Size = size;        //指定大小
       bk.NbPages = nbPages;  //指定页数
                            bk.IsRead = isread;    //指定是否阅读
       this._Books.Add(bk);   //加载到书签列表
      }
     }
    }
   }
   catch( Exception err ) //如果出现加载异常
   {   //在异常处理窗口中显示异常信息
    ExceptionManagement.Manage("Catalog:LoadBooks", err);
   }
   finally
   {
    stream.Close(); //文件流使用完成后要关闭以释放非托管资源
   }
   return result;      //返回结果
  }
LoadBooks要根据保存的顺序从二进制文件中反序列化保存的数据,因此代码首先实例化了一个二进制序列化对象

BinaryFormatter,然后使用FileStream打开文件,使用二进制序列化对象一步一步地进行反序列化。

如果文件的路径与当前反序列化的文件路径不一样,那么系统会初始化一个新的Books集合,并返回加载失败。

如果位于同一文件夹,将继续反序列化流中保存的图书,首先得到图书的数量,然后循环依次反序列化图书文件的详细信息。

如果图书文件存在,则实例化一个新的RarBook 对象,并使用反序列化的信息初始化这个对象,然后加载到图列表中。

2、3 加载图书封面
加载图书封面的LoadCovers() 方法,该方法将在一个后台线程中实现封面的加载,封面将被异步地加载到用户界面的ListBox中。

因为封面资料被保存到另一个二进制文件中,也需要使用反序列化从流中加载信息。

[csharp]
public void LoadCovers(object fileName)//加载封面  
        { 
            IFormatter formatter = new BinaryFormatter();//实例化二进制格式化器  
            Stream streamBin =   //加载封面文件  
                new FileStream((string)fileName, 
                FileMode.Open, 
                FileAccess.Read, 
                FileShare.None); 
            try 
            {   //反序列化图书数目  
                int count = (int)formatter.Deserialize(streamBin); 
                for (int i = 0; i < count; i++)  //遍历图书数目  
                {    //反序列化文件路径  
                    string filePath =  
                        (string)formatter.Deserialize(streamBin); 
                    //反序列化内存流,这个过程即便不存在内存流也需要进行反序列化  
                    MemoryStream coverStream =  
                        (MemoryStream)formatter.Deserialize(streamBin); 
                    foreach (IBook book in this._Books)  //遍历图书列表  
                    { 
                        if (book.FilePath == filePath) //如果文件路径相同  
                        { 
                            MemoryStream stream2 = new MemoryStream();//新建一个内存流  
                            coverStream.WriteTo(stream2);//将封面流写入内存流中   
                            coverStream.Flush();         //刷新封面流  
                            coverStream.Close();         //关闭封面流  
                            stream2.Position = 0;        //重定位内存流  
                            //调用Invoke方法,在与UI相同的线程中异步的更新图片  
                            Application.Current.Dispatcher.Invoke 
                                (DispatcherPriority.Normal, (ThreadStart)delegate 
                            { 
                                BitmapImage myImage = new BitmapImage(); 
                                myImage.BeginInit();  //开始更新  
                                myImage.StreamSource = stream2;//指定流来源  
                                myImage.DecodePixelWidth = 70;//指定图片宽度  
                                myImage.EndInit();           //结束更新  
                                book.Cover = myImage;   //将图书封面指定为该BitmapImage  
                            }); 
                            coverStream = null;  //释放封面流  
                            stream2 = null;      //释放内存流  
                        } 
                    } 
                } 
            } 
            catch (Exception err)//如果产生异常  
            {   //在与UI相同的线程中调用异常显示窗口  
                Application.Current.Dispatcher.Invoke 
                    (DispatcherPriority.Normal, (ThreadStart)delegate 
                {   //使用自定义的ExceptionManagement类  
                    ExceptionManagement.Manage("Catalog:LoadCovers", err); 
                }); 
            } 
            finally 
            { 
                streamBin.Close();//关闭文件流以释放资源  
            } 
        } 

public void LoadCovers(object fileName)//加载封面
  {
   IFormatter formatter = new BinaryFormatter();//实例化二进制格式化器
   Stream streamBin =   //加载封面文件
                new FileStream((string)fileName,
    FileMode.Open,
    FileAccess.Read,
    FileShare.None);
   try
   {   //反序列化图书数目
    int count = (int)formatter.Deserialize(streamBin);
    for (int i = 0; i < count; i++)  //遍历图书数目
    {    //反序列化文件路径
     string filePath =
                        (string)formatter.Deserialize(streamBin);
                    //反序列化内存流,这个过程即便不存在内存流也需要进行反序列化
                    MemoryStream coverStream =
                        (MemoryStream)formatter.Deserialize(streamBin);
     foreach (IBook book in this._Books)  //遍历图书列表
     {
      if (book.FilePath == filePath) //如果文件路径相同
      {
       MemoryStream stream2 = new MemoryStream();//新建一个内存流
       coverStream.WriteTo(stream2);//将封面流写入内存流中
       coverStream.Flush();         //刷新封面流
       coverStream.Close();         //关闭封面流
       stream2.Position = 0;        //重定位内存流
                            //调用Invoke方法,在与UI相同的线程中异步的更新图片
       Application.Current.Dispatcher.Invoke
                                (DispatcherPriority.Normal, (ThreadStart)delegate
       {
        BitmapImage myImage = new BitmapImage();
        myImage.BeginInit();  //开始更新
        myImage.StreamSource = stream2;//指定流来源
        myImage.DecodePixelWidth = 70;//指定图片宽度
        myImage.EndInit();           //结束更新
        book.Cover = myImage;   //将图书封面指定为该BitmapImage
       });
       coverStream = null;  //释放封面流
       stream2 = null;      //释放内存流
      }
     }
    }
   }
   catch (Exception err)//如果产生异常
   {   //在与UI相同的线程中调用异常显示窗口
    Application.Current.Dispatcher.Invoke
                    (DispatcherPriority.Normal, (ThreadStart)delegate
    {   //使用自定义的ExceptionManagement类
     ExceptionManagement.Manage("Catalog:LoadCovers", err);
    });
   }
   finally
   {
    streamBin.Close();//关闭文件流以释放资源
   }
  }
LoadCovers()在后台线程中执行,而该线程与UI不处于同一线程,要调用UI线程中的方法,必须使用Dispatcher.Invoke()方法,

传入要执行的方法。

 2、4 多线程图书搜索
现在回到Load()方法中,如果在加载书签失败或不存在书签文件,那么Load()方法会调用ParseDirectoryThread()方法在一个后台

线路中递归文件夹,得到书签信息。

[csharp]
internal void ParseDirectoryThread()//使用后台线程获取图书书签信息  
    { 
        try 
        { 
            Books.Clear();//清除图书列表  
               Thread t = new Thread  //在后台线程中调用ParseDirectoryRecursive方法  
                   (new ParameterizedThreadStart(ParseDirectoryRecursive)); 
            t.IsBackground = true;  //指定为后台线程  
            t.Priority = ThreadPriority.BelowNormal;//指定线程优先级别  
            t.Start(_bookPath); //为线程方法传入文件夹路径  
        } 
        catch (Exception err)  //如果产生异常  
        {   //调用自定义的异常信息窗口  
            ExceptionManagement.Manage("Catalog:ParseDirectoryThread", err); 
        } 
    } 

 internal void ParseDirectoryThread()//使用后台线程获取图书书签信息
  {
   try
   {
    Books.Clear();//清除图书列表
                Thread t = new Thread  //在后台线程中调用ParseDirectoryRecursive方法
                    (new ParameterizedThreadStart(ParseDirectoryRecursive));
    t.IsBackground = true;  //指定为后台线程
    t.Priority = ThreadPriority.BelowNormal;//指定线程优先级别
    t.Start(_bookPath); //为线程方法传入文件夹路径
   }
   catch (Exception err)  //如果产生异常
   {   //调用自定义的异常信息窗口
    ExceptionManagement.Manage("Catalog:ParseDirectoryThread", err);
   }
  }

ParseDirectoryThread方法首先清除图书列表,然后实例化一个参数化的线程,在后台线程中调用ParseDirectoryRecursive递归解析

传入的文件夹路径。该方法实现了重获书签信息的核心逻辑。

[csharp]
internal void ParseDirectoryRecursive(object path)//递归获取图书书签信息  
        { 
            try 
            {   //实例化DirectoryInfo对象  
                DirectoryInfo directory = new DirectoryInfo((string)path); 
                if (!directory.Exists)//如果目录不存在  
                {   //在UI线程中显示提示信息  
                    Application.Current.Dispatcher.Invoke 
                        (DispatcherPriority.Normal, (ThreadStart)delegate 
                    { 
                        MessageBox.Show("目录不存在! 请检查选项对话框"); 
                    }); 
                    return; //退出方法  
                } //如果目录存在,则调用GetFiles方法获取目录下所有的文件  
                foreach (FileInfo file in directory.GetFiles("*.*")) 
                {   //判断图书文件扩展名列表中是否包含指定文件的扩展名  
                    if (Properties.Settings.Default. 
                        BookFilter.Contains(file.Extension.ToUpper())) 
                    {   //如果包含,则在UI线程中实例化RarBook对象  
                        Application.Current.Dispatcher.Invoke 
                            (DispatcherPriority.Background, (ThreadStart)delegate 
                        {   //实例化一个新的RarBook对象  
                            IBook bk = (IBook)new RarBook(file.FullName, true); 
                            bk.Size = file.Length; //指定文件大小  
                            Books.Add(bk);         //添加到列表中  
                            this.IsChanged = true;//设置Ischanged状态为true  
                        }); 
                    } 
                } 
                foreach (DirectoryInfo dir in  //循环遍历目录下的子目录  
                    directory.GetDirectories("*", SearchOption.TopDirectoryOnly)) 
                {  //通过递归调用自身搜索子目录中的文件  
                    ParseDirectoryRecursive(dir.FullName); 
                } 
            } 
            catch (Exception err)  //如果产生了异常  
            {   //在UI线程中调用ExceptionManagement的Manage方法  
                Application.Current.Dispatcher.Invoke 
                    (DispatcherPriority.Normal, (ThreadStart)delegate 
                {   //在UI线程中显示异常信息  
                    ExceptionManagement.Manage("Catalog:ParseDirectoryRecursive", err); 
                }); 
                return;//方法返回  
            } 
            return; //方法返回  
        } 

internal void ParseDirectoryRecursive(object path)//递归获取图书书签信息
  {
   try
   {   //实例化DirectoryInfo对象
    DirectoryInfo directory = new DirectoryInfo((string)path);
    if (!directory.Exists)//如果目录不存在
    {   //在UI线程中显示提示信息
                    Application.Current.Dispatcher.Invoke
                        (DispatcherPriority.Normal, (ThreadStart)delegate
                    {
                        MessageBox.Show("目录不存在! 请检查选项对话框");
                    });
     return; //退出方法
    } //如果目录存在,则调用GetFiles方法获取目录下所有的文件
    foreach (FileInfo file in directory.GetFiles("*.*"))
    {   //判断图书文件扩展名列表中是否包含指定文件的扩展名
                    if (Properties.Settings.Default.
                        BookFilter.Contains(file.Extension.ToUpper()))
     {   //如果包含,则在UI线程中实例化RarBook对象
      Application.Current.Dispatcher.Invoke
                            (DispatcherPriority.Background, (ThreadStart)delegate
      {   //实例化一个新的RarBook对象
                            IBook bk = (IBook)new RarBook(file.FullName, true);
       bk.Size = file.Length; //指定文件大小
       Books.Add(bk);         //添加到列表中
       this.IsChanged = true;//设置Ischanged状态为true
      });
     }
    }
    foreach (DirectoryInfo dir in  //循环遍历目录下的子目录
                    directory.GetDirectories("*", SearchOption.TopDirectoryOnly))
    {  //通过递归调用自身搜索子目录中的文件
     ParseDirectoryRecursive(dir.FullName);
    }
   }
   catch (Exception err)  //如果产生了异常
   {   //在UI线程中调用ExceptionManagement的Manage方法
    Application.Current.Dispatcher.Invoke
                    (DispatcherPriority.Normal, (ThreadStart)delegate
    {   //在UI线程中显示异常信息
     ExceptionManagement.Manage("Catalog:ParseDirectoryRecursive", err);
    });
    return;//方法返回
   }
   return; //方法返回
  }
ParseDirectoryRecursive是一个不断调用自身的过程。

因为Books这个集合是一个泛型的ObservableCollection类,该类将要与UI进行绑定来自动更新UI,

而IBook实现了INotifyPropertyChanged接口。同样地,在一本书的属性信息变化时触发UI的变更,对于Books集合的

增删改必须要与UI处于同一线程,因此使用了Dispatcher 的Invoke()方法。

2、5 保存图书信息
[csharp]
public void Save()//保存封面和书签信息  
        { 
            try 
            { 
                if (IsChanged) //如果图书列表发生变化  
                {   //移除没有封面的图书                   
                    RemoveDirtyBooks(); 
                    //保存书名和书签  
                    string bin = System.Reflection.Assembly.//获取书签文件名  
                        GetExecutingAssembly().Location.Replace(".exe", ".bin"); 
                    SaveBooks(bin);//调用SaveBooks方法保存书签                    
                    bin = System.Reflection.Assembly.  //获取封面文件名  
                        GetExecutingAssembly().Location.Replace(".exe", ".bin2"); 
                    SaveCovers(bin);//调用SaveCovers方法保存封面信息  
                } 
            } 
            catch (Exception err)//如果触发异常  
            {   //显示异常提示窗口  
                ExceptionManagement.Manage("Catalog:Save", err); 
            } 
        } 

public void Save()//保存封面和书签信息
  {
   try
   {
    if (IsChanged) //如果图书列表发生变化
                {   //移除没有封面的图书     
     RemoveDirtyBooks();
     //保存书名和书签
     string bin = System.Reflection.Assembly.//获取书签文件名
                        GetExecutingAssembly().Location.Replace(".exe", ".bin");
     SaveBooks(bin);//调用SaveBooks方法保存书签     
     bin = System.Reflection.Assembly.  //获取封面文件名
                        GetExecutingAssembly().Location.Replace(".exe", ".bin2");
     SaveCovers(bin);//调用SaveCovers方法保存封面信息
    }
   }
   catch (Exception err)//如果触发异常
   {   //显示异常提示窗口
    ExceptionManagement.Manage("Catalog:Save", err);
   }
  }

 

2、6 刷新图书列表
在UI主线程中,当用户单击“刷新”按钮时会调用Catalog 类的Refresh() 方法,因为图书阅读器是基于文件和文件夹这种存储模式,文件和文件夹可能会发生变化。

那么通过刷新机制可以从事Books集合中移除不存在的文件或文件夹。Refresh使用一个后台线程调用RefreshThread()方法。

[csharp]
// 从目录中加载图书列表  
       public void Refresh() 
       { 
           try 
           { 
               // 带参数的线程委托  
               Thread t = new Thread(new ParameterizedThreadStart(RefreshThread)); 
               t.IsBackground = true; 
               t.Priority = ThreadPriority.BelowNormal; 
               t.Start(_bookPath); 
           } 
           catch (Exception err) 
           { 
            ExceptionManagement.Manage("Catalog:Refresh", err); 
           } 
       } 

 // 从目录中加载图书列表
        public void Refresh()
        {
            try
            {
                // 带参数的线程委托
                Thread t = new Thread(new ParameterizedThreadStart(RefreshThread));
                t.IsBackground = true;
                t.Priority = ThreadPriority.BelowNormal;
                t.Start(_bookPath);
            }
            catch (Exception err)
            {
    ExceptionManagement.Manage("Catalog:Refresh", err);
            }
        }
 

[csharp]
internal void RefreshThread(object o)//刷新图书列表  
       { 
           try 
           {   //首先,刷新己经不存在的图书    
               // List也为泛型类型  
               List temp = new List(); 
               foreach (IBook book in this._Books) 
               {   //循环遍历判断图书文件是否存在  
                   if (!File.Exists(book.FilePath)) 
                       temp.Add( book );//不存在则加入到移除图书列表  
               } 
               foreach (IBook book in temp)//遍历要移除的图书列表  
                   Application.Current.Dispatcher.Invoke  //  因为refresh是在后台,而我们的移除的内容是在用户界面,  
                                                          // 所以要用主线程中的方法  
                       (DispatcherPriority.Normal, (ThreadStart)delegate 
                   {   
                       //在UI线程中移除图书  
                       // 因为_Books是与ListBox绑定的变量,这样就能在界面上删除图书  
                       _Books.Remove(book); 
                   }); 
               //重新从文件中加入图书列表  
               ParseDirectoryRecursiveWithCheck(_bookPath); 
           } 
           catch (Exception err)//如果产生异常  
           { 
               Application.Current.Dispatcher.Invoke 
                   (DispatcherPriority.Normal, (ThreadStart)delegate 
               {   //在UI线程中显示异常处理窗口  
                   ExceptionManagement.Manage("Catalog:RefreshThread", err); 
               }); 
           } 
       } 

 internal void RefreshThread(object o)//刷新图书列表
        {
            try
            {   //首先,刷新己经不存在的图书 
                // List也为泛型类型
                List temp = new List();
                foreach (IBook book in this._Books)
                {   //循环遍历判断图书文件是否存在
                    if (!File.Exists(book.FilePath))
                        temp.Add( book );//不存在则加入到移除图书列表
                }
                foreach (IBook book in temp)//遍历要移除的图书列表
                    Application.Current.Dispatcher.Invoke  //  因为refresh是在后台,而我们的移除的内容是在用户界面,
                                                           // 所以要用主线程中的方法
                        (DispatcherPriority.Normal, (ThreadStart)delegate
                    { 
                        //在UI线程中移除图书
                        // 因为_Books是与ListBox绑定的变量,这样就能在界面上删除图书
                        _Books.Remove(book);
                    });
                //重新从文件中加入图书列表
                ParseDirectoryRecursiveWithCheck(_bookPath);
            }
            catch (Exception err)//如果产生异常
            {
                Application.Current.Dispatcher.Invoke
                    (DispatcherPriority.Normal, (ThreadStart)delegate
                {   //在UI线程中显示异常处理窗口
                    ExceptionManagement.Manage("Catalog:RefreshThread", err);
                });
            }
        }
代码首先实例化一个新的List泛型列表,循环Books集合,判断指定的图书对应的文件是否存在,如果不存在则加入到List中准备移除,

然后在UI线程中进行循环移除;

然后调用ParseDirectoryRecursiveWithCheck()方法,该方法与ParseDirectoryRecursive类似,是一个递归方法,该方法主要不同的是调用了

BookExist() 方法来判断图书的文件路径与当前的文件路径是否一致。

2、7  定义图书接口IBook
BookReader当前支持的图书类型有限,仅RarBook这一类,但是系统在最初架构时,已经提供了弹性方式允许将来扩充

多种图书文件格式,其BaseBook实现了IBook接口,开发人员可以通过派生自BaseBook类来实现多种格式图书类。

 

BaseBook实现了IBook接口,IBook接口又被Catelog引用,使用这种基于接口的方法可以实现程序间的解耦,

使程序具有良好的可扩充性。

[csharp]
internal interface IBook : INotifyPropertyChanged 
    { 
        string FileName { get; }//文件名称  
        int NbPages { get; set; }//图书页数  
        long Size { get; set; }  //文件大小  
        bool IsRead { get; set; } //是否阅读  
        string Bookmark { get; set; }//书签  
        BitmapImage Cover { get; set; } //封面  
        IBookItem CurrentPage { get; set; }//书页  
        string FilePath { get; set; }  //文件路径//书页集合  //图像缓存  
        BitmapImage GetCurrentPageImage();//当前书页  
        void GotoMark();  //定位书签  
        bool GotoNextPage();//转到下一页  
        bool GotoPage(IBookItem page);//定位到指定页  
        bool GotoPreviousPage();//转到上一页  
        void Load(); //加载图书  
        void ManageCache();//管理缓存  
        void SetMark(); //设置书签  
        void UnLoad();  //卸载图书  
    } 

internal interface IBook : INotifyPropertyChanged
 {
  string FileName { get; }//文件名称
  int NbPages { get; set; }//图书页数
  long Size { get; set; }  //文件大小
        bool IsRead { get; set; } //是否阅读
  string Bookmark { get; set; }//书签
  BitmapImage Cover { get; set; } //封面
  IBookItem CurrentPage { get; set; }//书页
        string FilePath { get; set; }  //文件路径//书页集合  //图像缓存
  BitmapImage GetCurrentPageImage();//当前书页
  void GotoMark();  //定位书签
  bool GotoNextPage();//转到下一页
  bool GotoPage(IBookItem page);//定位到指定页
  bool GotoPreviousPage();//转到上一页
  void Load(); //加载图书
  void ManageCache();//管理缓存
  void SetMark(); //设置书签
  void UnLoad();  //卸载图书
 }
IBook接口定义了一本书基本属性和方法,该接口派生自INotifyPropertyChanged接口,当图书信息发生变化时,

可以向UI触发属性变更通知。

2、8 图书基类BaseBook
BaseBook也要实现INotifyPropertyChanged接口的成员员,BaseBook类与其他类的关系如下:

 C#入门学习-----图书阅读器(WPF 用户控件技术)_第4张图片

RarBook从BaseBook基类派生,提供了对于Rar格式图书的实现。BaseBook包括ImageCache图像缓存。

BaseBook的Pages包含实现了IBookItem接口的对象集合,CurrentPage用于显示当前的图书页面。

该类重载了构造函数,提供了一个接收文件路径的构造函数,当文件路径发生变化时,会触发INotifyPropertyChanged接口

中定义的变更通知。

[csharp]
public BaseBook(string filePath) 
        { 
            _filePath = filePath; //得到文件路径  
            RaisePropetyChanged("FilePath");//触发文件路径变更通知  
            RaisePropetyChanged("FileName");//触发文件变更通知  
        } 

public BaseBook(string filePath)
  {
   _filePath = filePath; //得到文件路径
   RaisePropetyChanged("FilePath");//触发文件路径变更通知
   RaisePropetyChanged("FileName");//触发文件变更通知
  }
下面从3个方面介绍BaseBook实现的功能:

(1) 实现书签功能: BaseBook允许用户定义或跳转到书签,提供书签列表功能。

[csharp]
public void SetMark() 
        {   //将当前页面的路径赋给Bookmark  
            Bookmark = _CurrentPage.FilePath; 
        } 
        public void GotoMark() 
        {   //如果_Bookmark路径不为空  
            if (!string.IsNullOrEmpty(_Bookmark)) 
                foreach (IBookItem pg in Pages) 
                {   //如果页面路径与书签路径相同  
                    if( pg.FilePath == _Bookmark ) 
                        _CurrentPage = pg; //指定当前页面  
                } 
            } 
        } 

public void SetMark()
  {   //将当前页面的路径赋给Bookmark
   Bookmark = _CurrentPage.FilePath;
  }
  public void GotoMark()
  {   //如果_Bookmark路径不为空
   if (!string.IsNullOrEmpty(_Bookmark))
    foreach (IBookItem pg in Pages)
    {   //如果页面路径与书签路径相同
     if( pg.FilePath == _Bookmark )
      _CurrentPage = pg; //指定当前页面
    }
   }
  }
SetMark()方法主要是记录当前页面的文件路径,该值被赋给Bookmark属性,而BookMark属性会触发属性变更通知,以便UI能够知晓变化。

(2)  实现页面导航:BaseBook提供上一页、下一页或定义到指定页。

[csharp]
public bool GotoPage( IBookItem page )//定位到指定页面  
        {   //循环遍历页面  
            foreach (IBookItem pg in Pages) 
            {   //如果页面与指定页面一致  
                if (pg == page) 
                {   //设置当前页面  
                    _CurrentPage = pg; 
                    return true;//返回设置成功  
                } 
            } 
            return false;//否则设置失败  
        } 
        public bool GotoNextPage()//跳转到下一页面  
        {   //得到当前页面的索引值  
            int next = Pages.IndexOf(_CurrentPage); 
            if (next >= Pages.Count-1)//判断是否越界  
                return false; //如果越界则返回  
            else 
            { 
                next = next + 1; //让索引值加1转到下一页  
                _CurrentPage = Pages[next];//设置当前页为下一页  
                return true; //返回设置成功标志  
            } 
        } 
        public bool GotoPreviousPage()//跳转到上一页面  
        {   //得到上一页面的索引值  
            int next = Pages.IndexOf(_CurrentPage); 
            if (next == 0) //如果值为0则不能再上一页  
                return false;//返回导航失败标记  
            else 
            { 
                next = next - 1;//减少一页  
                _CurrentPage = Pages[next];//设置当前页为上一页  
                return true;  //返回设置成功标记  
            } 
        } 

public bool GotoPage( IBookItem page )//定位到指定页面
  {   //循环遍历页面
   foreach (IBookItem pg in Pages)
   {   //如果页面与指定页面一致
    if (pg == page)
    {   //设置当前页面
     _CurrentPage = pg;
     return true;//返回设置成功
    }
   }
   return false;//否则设置失败
  }
  public bool GotoNextPage()//跳转到下一页面
  {   //得到当前页面的索引值
   int next = Pages.IndexOf(_CurrentPage);
   if (next >= Pages.Count-1)//判断是否越界
    return false; //如果越界则返回
   else
   {
    next = next + 1; //让索引值加1转到下一页
    _CurrentPage = Pages[next];//设置当前页为下一页
    return true; //返回设置成功标志
   }
  }
  public bool GotoPreviousPage()//跳转到上一页面
  {   //得到上一页面的索引值
   int next = Pages.IndexOf(_CurrentPage);
            if (next == 0) //如果值为0则不能再上一页
                return false;//返回导航失败标记
            else
            {
                next = next - 1;//减少一页
                _CurrentPage = Pages[next];//设置当前页为上一页
                return true;  //返回设置成功标记
            }
  }
 

(3) 实现基本的图像缓存: BaseBook提供了基本的缓存功能。

2、9 图书页面接口IBookItem的定义
[csharp]
internal interface IBookItem //图书页面接口  
    { 
        string FilePath { get; }//文件路径  
        string FileName { get; }//文件名称  
    } 

internal interface IBookItem //图书页面接口
 {
  string FilePath { get; }//文件路径
  string FileName { get; }//文件名称
 }

 3、设计BookReader用户主界面
3、1 设计系统主界面
WPF用户界面的设计与传统的Winodw Forms的UI设计有了明显的区别,在WPF中,UI设计通常使用

而局软件进行UI布局。

BookReader的主界面使用一个Grid控件将整个面板分为4行。因了BookReader要显示圆滑的边框,所以需要将主窗口的背景设置为透明色,

并且去掉Windows自带的标题栏。在声明属性时,将其Backgroud属性设置为Transparent,设置WindowsStyle为None。

 \

添加一个Grid控件,使用该Grid将覆盖整个客户端区域使用RowDefinitions集合编辑器将这个grid划分为4行。

 

 加载中...

 

因为将WindowStyle属性设置为None后,需要为主界面自己添加最小化、最大化和关闭按钮。

窗体阅读区域位于第3行,在该行内部又嵌入一个Grid,这个Grid将中间分为3列,分别用于旋转ListBox,Splitter和一个用来显示书面的图像的用户控件。

 加载中...

 

3、2 实现主窗口样式的绑定
在主界面中,大多数控件的Style使用了DynamicResource这个动态资源关键字绑定到了样式。

在WPF中,资源分为以下两类:

(1)静态资源:使用StaticResource进行指定,静态资源在第一次编译后即确定其对象或值,之后不可修改。

(2)动态资源:使用DynamicResource指定,在运行时决定,当运行时才会到资源目录中查找其值。

例如,在主界面XAML文件中,Border用来为主界面实现圆角边框,使得窗体看起来很圆滑,一些按钮具有特别样式,都使用

DynamicResource进行设定。

 加载中...

 

那么这些资源是定义在哪里呢?打开App.xaml文件,就可以看到在应用程序集,使用ResourceDictionary合并了几个资源文件。

 加载中...

3、3 实现图书列表界面
图书列表信息被绑定到一个ListBox控件上,图书阅读面板上使用了一个控件来显示图片信息,中间使用一个自定义的GridSplitterExpander控件来实现分割条。

ListBox控件的DataContext将绑定到Catalog对象的Books集合上,用来显示图书封面和图书详细信息。

当用户双击图书时,在图书阅读界面显示图书,该ListBox控件被放在一个Grid的左侧列中。

 加载中...

 

每当加载图书目录时,ListBox会被绑定到Catalog的Books集合上,加载图书目录的代码写在LoadCatalog() 方法内部,

该方法在主窗体加载时会被调用。

[csharp]
private void LoadCatalog() 
        { 
            //加载图书目录  
            _Catalog.Load(Properties.Settings.Default.Catalog); 
            //指定ListBox控件的数据绑定  
            CatalogListBox.DataContext = _Catalog.Books; 
            this.Splitter.Title = //指定中间分割条的文本  
                string.Format("CATALOG ({0} book(s))", _Catalog.Books.Count); 
        } 

private void LoadCatalog()
  {
   //加载图书目录
   _Catalog.Load(Properties.Settings.Default.Catalog);
            //指定ListBox控件的数据绑定
   CatalogListBox.DataContext = _Catalog.Books;
   this.Splitter.Title = //指定中间分割条的文本
                string.Format("CATALOG ({0} book(s))", _Catalog.Books.Count);
  }

CatalogListBox的DataContext被指定到_Catalog.Books集合,然后ListBox的ItemSource指定到了集合中的元素,

具体的呈现交给了命名样式CatalogCoverStyle。(CatalogCoverStyle通过下图指定)

 加载中...

该样式位于Resources文件夹下的Shared.xmal的定义中,指定了ListBox的数据模板和而已面板。

(Shared.xmal是在App.xml中被合并到资源中了)

 加载中...

Shared.xmal中定义了三部分内容:

(1)ListBox的整体数据模板

 加载中...

(2)单个元素指定的样式

 加载中...

(3)BaseBook的数据模板

 加载中...

 

3、4 实现图书阅读界面
图书阅读面板中间是一个可折叠的自定义控件,该控件也显示了当前打开图书的页数,可以单击上面的方向箭头进行折叠和展开。

分割条与PageView控件的声明XAML如下:

 加载中...

 
4、实现用户界面功能
本节将介绍如何调用核心层中的功能来实现阅读器的运行。

4、1 实现工具按钮事件
最大化和最小化只是改变主窗体的WindowState来控制;

对于退出按钮,可以直接调用主窗体的Close()方法来关闭窗体,实现如下:

 加载中...

[csharp]
//标题栏的关闭按钮事件处理代码  
      private void closeButton_Click(object sender, RoutedEventArgs e) 
      { 
          this.Close();//Close方法关闭主窗体  
      } 
      //标题栏的最大化按钮事件处理代码  
      private void maximizeButton_Click(object sender, RoutedEventArgs e) 
      {   //首先判断当前WindowState的状态是否是最大化  
          if (this.WindowState == WindowState.Maximized) 
               //如果为最大化,则设置为标准样式  
              this.WindowState = WindowState.Normal; 
          else //否则设置为最大化样式  
              this.WindowState = WindowState.Maximized; 
      } 
      //将窗口最小化事件处理代码  
      private void minimizeButton_Click(object sender, RoutedEventArgs e) 
      { 
          this.WindowState = WindowState.Minimized;//指定最小化  
      } 

  //标题栏的关闭按钮事件处理代码
        private void closeButton_Click(object sender, RoutedEventArgs e)
        {
            this.Close();//Close方法关闭主窗体
        }
        //标题栏的最大化按钮事件处理代码
        private void maximizeButton_Click(object sender, RoutedEventArgs e)
        {   //首先判断当前WindowState的状态是否是最大化
            if (this.WindowState == WindowState.Maximized)
                 //如果为最大化,则设置为标准样式
                this.WindowState = WindowState.Normal;
            else //否则设置为最大化样式
                this.WindowState = WindowState.Maximized;
        }
        //将窗口最小化事件处理代码
        private void minimizeButton_Click(object sender, RoutedEventArgs e)
        {
            this.WindowState = WindowState.Minimized;//指定最小化
        }
4、1、1、页面适应按钮
调整宽度和高度的按钮,其事件处理代码通过调用用户控件PageViewer的方法来实现,代码如下:

[csharp]
private void btnFitWidth_Click(object sender, RoutedEventArgs e) 
      {  //通过设置PageViewer控件的FitWidth属性来设置宽度  
    this.SimplePageView.FitWidth(); 
      } 
      private void btnFitHeight_Click(object sender, RoutedEventArgs e) 
      {   //通过设置PageViewer控件的FitHeight来设置宽度  
    this.SimplePageView.FitHeight(); 
      }      

  private void btnFitWidth_Click(object sender, RoutedEventArgs e)
        {  //通过设置PageViewer控件的FitWidth属性来设置宽度
   this.SimplePageView.FitWidth();
        }
        private void btnFitHeight_Click(object sender, RoutedEventArgs e)
        {   //通过设置PageViewer控件的FitHeight来设置宽度
   this.SimplePageView.FitHeight();
        }  
4、1、2 打开图书按钮
[csharp]
///打开一个不在当前文件夹的外部文件       
        private void btnOpen_Click(object sender, RoutedEventArgs e) 
        { 
            using (System.Windows.Forms.OpenFileDialog  //实例化一个OpenFileDialog对象  
                browser = new System.Windows.Forms.OpenFileDialog()) 
            { 
                if (browser.ShowDialog() == //显示打开文件对话框  
                    System.Windows.Forms.DialogResult.OK) 
                {   //调用Catalog的Open方法打开文件,将返回的IBook实例加载到图书列表  
                    LoadBook( (IBook)_Catalog.Open(browser.FileName) ); 
                } 
            } 
        }        

///打开一个不在当前文件夹的外部文件  
  private void btnOpen_Click(object sender, RoutedEventArgs e)
  {
   using (System.Windows.Forms.OpenFileDialog  //实例化一个OpenFileDialog对象
                browser = new System.Windows.Forms.OpenFileDialog())
   {
    if (browser.ShowDialog() == //显示打开文件对话框
                    System.Windows.Forms.DialogResult.OK)
    {   //调用Catalog的Open方法打开文件,将返回的IBook实例加载到图书列表
     LoadBook( (IBook)_Catalog.Open(browser.FileName) );
    }
   }
  }      
在代码中,使用using语句块实例化一个OpenFileDialog对象,在超过该using语句块的作用域时,该对象将自动

释放掉。根据获取到的所要打开的文件名, 调用Catalog对象的Open() 方法打开该文件。

然后调用LoadBook将打开的文件加载到PageViewer控件及图书列表中。

LoadBook是定义在主窗体中的一个辅助方法,该方法专用于加载指定IBook对象实例的图书到UI对象PageViewer控件中。

[csharp]
//加载一部指定的图书  
    private void LoadBook( IBook book ) 
    { 
        try 
        { 
            if (_CurrentBook != null)//首先卸载CurrentBook图书  
                _CurrentBook.UnLoad(); 
            _CurrentBook = book;//将当前图书指定为传入的图书  
            _CurrentBook.Load();//加载图书到图书对象中  
               this.SimplePageView.Scale = 1.0;//指定缩放比率  
               //指定当前图书页面图像  
               this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage(); 
               //指定当前Label控件显示图书路径  
            this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath; 
               //滚动到图书的开始位置  
            this.SimplePageView.ScrollToHome(); 
        } 
        catch (Exception err) 
        {   //如果产生异常,显示异常信息窗口  
            ExceptionManagement.Manage("Main:LoadBook", err); 
        } 
    } 

 //加载一部指定的图书
  private void LoadBook( IBook book )
  {
   try
   {
    if (_CurrentBook != null)//首先卸载CurrentBook图书
     _CurrentBook.UnLoad();
    _CurrentBook = book;//将当前图书指定为传入的图书
    _CurrentBook.Load();//加载图书到图书对象中
                this.SimplePageView.Scale = 1.0;//指定缩放比率
                //指定当前图书页面图像
                this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage();
                //指定当前Label控件显示图书路径
    this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath;
                //滚动到图书的开始位置
    this.SimplePageView.ScrollToHome();
   }
   catch (Exception err)
   {   //如果产生异常,显示异常信息窗口
    ExceptionManagement.Manage("Main:LoadBook", err);
   }
  }
 

在代码中,产生指定_CurrentBook对象,当前一本书加载进来时,将设置_CurrentBook对象,然后将PageViewer控件的Source

指定从GetCurrentPageImage()方法返回的当前页面。

4、1、3 选项按钮
[csharp]
private void btnOptions_Click(object sender, RoutedEventArgs e) 
        { //显示选项对话框  
            try 
            {   //实例化选项对话框  
                OptionWindow dlg = new OptionWindow(); 
                if (dlg.ShowDialog() == true)//显示对话框  
                {   //如果变更了属性设置  
                    if (dlg.NeedToReload) 
                    {  //重新加载整个图书  
                        LoadCatalog(); 
                    } 
                } 
            } 
            catch (Exception err)//出现异常  
            {   //显示异常窗口  
                ExceptionManagement.Manage("Main:btnOptions_Click", err); 
            } 
        } 

private void btnOptions_Click(object sender, RoutedEventArgs e)
  { //显示选项对话框
   try
   {   //实例化选项对话框
    OptionWindow dlg = new OptionWindow();
    if (dlg.ShowDialog() == true)//显示对话框
    {   //如果变更了属性设置
     if (dlg.NeedToReload)
     {  //重新加载整个图书
      LoadCatalog();
     }
    }
   }
   catch (Exception err)//出现异常
   {   //显示异常窗口
    ExceptionManagement.Manage("Main:btnOptions_Click", err);
   }
  }
4、1、4 全屏按钮
[csharp]
private void btnFullScreen_Click(object sender, RoutedEventArgs e) 
        { 
            try 
            { 
                if (_isFullSreen)//当前是否全屏的布尔字段  
                {   //恢复全屏状态  
                    this.WindowState = WindowState.Normal; 
                    _isFullSreen = false;  //重置全屏布尔字段  
                    Splitter.IsExpanded = false; 
                } 
                else //如果当前不是全屏状态  
                {   //将窗口最大化  
                    this.WindowState = WindowState.Maximized; 
                    _isFullSreen = true;//设置全屏状态  
                    Splitter.IsExpanded = true;//将分割条进行折叠  
                } 
            } 
            catch (Exception err)//如果出现错误   
            {   //显示异常并记录错误信息  
                ExceptionManagement.Manage("Main:btnFullScreen_Click", err); 
            } 
        } 

private void btnFullScreen_Click(object sender, RoutedEventArgs e)
  {
   try
   {
    if (_isFullSreen)//当前是否全屏的布尔字段
    {   //恢复全屏状态
     this.WindowState = WindowState.Normal;
     _isFullSreen = false;  //重置全屏布尔字段
                    Splitter.IsExpanded = false;
    }
    else //如果当前不是全屏状态
    {   //将窗口最大化
     this.WindowState = WindowState.Maximized;
     _isFullSreen = true;//设置全屏状态
                    Splitter.IsExpanded = true;//将分割条进行折叠
    }
   }
   catch (Exception err)//如果出现错误
   {   //显示异常并记录错误信息
    ExceptionManagement.Manage("Main:btnFullScreen_Click", err);
   }
  }
将WindowState设置为Maximized来使窗口最大化而实现全屏,同时使自定义的控件Splitter控件进行折叠以模拟全屏效果。

 

4、2 实现上下文菜单事件处理
 

 

添加标签、定位到标签和移除标签菜单项的实现代码如下:

 

 加载中...

 加载中...

[csharp]
//将当前图书的当前页面设置为书签  
    private void MenuItem_BookMark(object sender, RoutedEventArgs e) 
    { 
        try 
        { 
            if (_CurrentBook != null)//仅在当前图书不空才能设置书签  
            { 
                _CurrentBook.SetMark();//将当前图书页面的路径指定为当前书签  
                _Catalog.IsChanged = true;//设置书签变更标志   
            } 
        } 
        catch (Exception err) 
        {   //如果产生异常显示异常信息  
            ExceptionManagement.Manage("Main:MenuItem_BookMark", err); 
        } 
    } 
       //如果当前图书有设置书签,则定位到当前书签          
    private void MenuItem_GotoBookMark(object sender, RoutedEventArgs e) 
    { 
        try 
        {   //判断当前图书是否是在图书列表中选择图书  
            if (_CurrentBook != (IBook)CatalogListBox.SelectedValue) 
            {   //如果不是,则重新加载选中的图书  
                LoadBook((IBook)CatalogListBox.SelectedValue); 
            } 
            _CurrentBook.GotoMark();//定位到书签页面  
               //获取当前页面的图书  
               this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage(); 
            this.SimplePageView.ScrollToHome();//滚动到页开始处  
        } 
        catch (Exception err) 
        {   //如果有异常显示异常信息  
            ExceptionManagement.Manage("Main:MenuItem_GotoBookMark", err); 
        } 
    } 
       //清除书签  
    private void MenuItem_ClearBookMark(object sender, RoutedEventArgs e) 
    { 
        try 
        {   //清除当前选中图书的书签  
            ((IBook)CatalogListBox.SelectedValue).Bookmark = string.Empty; 
            _Catalog.IsChanged = true;//设置IsChanged标志以便保存书签  
        } 
        catch (Exception err) 
        {  //如果产生异常,显示异常处理信息  
            ExceptionManagement.Manage("Main:MenuItem_ClearBookMark", err); 
        } 
    } 

 //将当前图书的当前页面设置为书签
  private void MenuItem_BookMark(object sender, RoutedEventArgs e)
  {
   try
   {
    if (_CurrentBook != null)//仅在当前图书不空才能设置书签
    {
     _CurrentBook.SetMark();//将当前图书页面的路径指定为当前书签
     _Catalog.IsChanged = true;//设置书签变更标志
    }
   }
   catch (Exception err)
   {   //如果产生异常显示异常信息
    ExceptionManagement.Manage("Main:MenuItem_BookMark", err);
   }
  }
        //如果当前图书有设置书签,则定位到当前书签       
  private void MenuItem_GotoBookMark(object sender, RoutedEventArgs e)
  {
   try
   {   //判断当前图书是否是在图书列表中选择图书
    if (_CurrentBook != (IBook)CatalogListBox.SelectedValue)
    {   //如果不是,则重新加载选中的图书
     LoadBook((IBook)CatalogListBox.SelectedValue);
    }
    _CurrentBook.GotoMark();//定位到书签页面
                //获取当前页面的图书
                this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage();
    this.SimplePageView.ScrollToHome();//滚动到页开始处
   }
   catch (Exception err)
   {   //如果有异常显示异常信息
    ExceptionManagement.Manage("Main:MenuItem_GotoBookMark", err);
   }
  }
        //清除书签
  private void MenuItem_ClearBookMark(object sender, RoutedEventArgs e)
  {
   try
   {   //清除当前选中图书的书签
    ((IBook)CatalogListBox.SelectedValue).Bookmark = string.Empty;
    _Catalog.IsChanged = true;//设置IsChanged标志以便保存书签
   }
   catch (Exception err)
   {  //如果产生异常,显示异常处理信息
    ExceptionManagement.Manage("Main:MenuItem_ClearBookMark", err);
   }
  }

 

书签只能对当前图书进行设置,因此需要具有_CurrentBook值,调用 SetMark()方法用于将当前阅读的路径记录到内部的BookMark属性中。

IsChanged属性设置为true后,在保存书签到文件时,会保存到文件中去。

UI端如何在图书列表上添加一个书签图标和阅读外观呢?

可以在XMAL中将一个Border和BitmapImage控件绑定到了IsRead和Bookmark属性上,可以参考Shared.xaml资源文件中的

CatalogCoverStyle 样式定义。

 

4、3  创建PageViewer用户控件
该控件在内部使用一个Image控件来显示图书,为了使图书阅读器与目前市面上流行的阅读器软件具有类似的功能,

该Image控件需要处理和种鼠标和键盘事件来实现图书阅读器效果。

 加载中...

 

4、4 PageViewer 控件属性定义
PageViewer定义了3个属性,这3个属性用来改变PageViewer的内部行为。

[csharp]
//指定自动缩放类型  
      public AutoFit AutoFitMode 
      { 
          get { return  //获取自动缩放属性值  
              (AutoFit)Properties.Settings.Default.UseAutoFit; } 
      } 
      //指定缩放的大小属性  
public double Scale 
      { 
          get { return _scale; } 
          set 
          { 
              _scale = value; 
              UpdateScale(); //更新屏幕缩放  
          } 
      } 
//要显示的图像源  
      public ImageSource Source 
      { 
          get { return this.PageImage.Source; } 
          set { this.PageImage.Source = value; } 
      } 
       

  //指定自动缩放类型
        public AutoFit AutoFitMode
        {
            get { return  //获取自动缩放属性值
                (AutoFit)Properties.Settings.Default.UseAutoFit; }
        }
        //指定缩放的大小属性
  public double Scale
        {
            get { return _scale; }
            set
            {
                _scale = value;
                UpdateScale(); //更新屏幕缩放
            }
        }
  //要显示的图像源
        public ImageSource Source
        {
            get { return this.PageImage.Source; }
            set { this.PageImage.Source = value; }
        }
       
在代码中,AutoFitMode属性根据用户在选项面板中的设置来指定自动缩放的类型;

 加载中...

Scale属性用来指定缩放大小,主要根据用户在右下角的Slider控件的返回值来设定Image控件的缩放大小;设置后会调用UpdateScale()方法,

该方法将使用ScaleTransform对象为Image控件设置缩放变换;

[csharp]
///

  
        /// 更新图像控件的缩放,并触发事件  
        ///
  
        private void UpdateScale() 
        { 
            this.scaleTransform.ScaleX = _scale;//指定x缩放值  
            this.scaleTransform.ScaleY = _scale;//指定y缩放值  
            //指定变换中心点  
            this.scaleTransform.CenterX = 0.5; 
            this.scaleTransform.CenterY = 0.5; 
            //触发变换事件  
            RaiseZoomChanged(); 
        } 

///


  /// 更新图像控件的缩放,并触发事件
  ///

        private void UpdateScale()
        {
            this.scaleTransform.ScaleX = _scale;//指定x缩放值
            this.scaleTransform.ScaleY = _scale;//指定y缩放值
            //指定变换中心点
            this.scaleTransform.CenterX = 0.5;
            this.scaleTransform.CenterY = 0.5;
            //触发变换事件
   RaiseZoomChanged();
        }


UpdateScale通过设置缩放变换的ScaleX、ScaleY指定缩放大小,最后调用RaiseZoomChanged解发缩放路由事件。

4、5 定义PageViewer控件路由事件
路由事件是一种可以针对元素树中的多个侦听器(而不是针对引发该事件的对象)调用处理程序的事件。

在WPF中,对用户界面进行布局与传统的Windows Forms有些不一样,在WPF中,用户界面是由一个对象树组成,称为逻辑树。在Vs2010中,用户可以在大纲视图

中看到整棵逻辑树。在WPF中,还有一棵树,称为可视树。可视树将所有的节点打散到核心的可视组件中,而不是将每个元素当作一个黑盒。

例如一个ListBox,在逻辑树上是一个单独的元素,但是在视觉上是由多个元素组成的。因为WPF中的这种特性,路由事件设计的目的是专门用于在元素树中使用的事件,

当路由事件解发后,事件可以向上或向下遍历视觉树和逻辑树,使用一种简单而持久的方式在每个元素上解发。

 

 

WPF中的路由事件是一个可传递的事件,事件可以沿着视觉树向上和向下传递,因此事件可以被多个视觉元素捕获来决定是否处理。

RaiseZoomChanged事件将在MainWindow.xmal中被订阅,以便在图像缩放后,能触发缩放滑动块自动切换位置行为。

在主窗体的XMAL声明中,关联了ZoomChanged事件,代码如下:

 加载中...

ZoomChanged 是一个路由事件,因此事件在解发后会以冒泡的形式通知其视觉树中的上层元素,使得视觉树的其他元素有机会处理该事件。

SimplePageView_ZoomChanged的代码如下:

[csharp]
///

  
        /// 当页面缩放后更新滑动条控件的位置  
        ///
  
        private void SimplePageView_ZoomChanged 
            (object sender, PageViewer.ZoomRoutedEventArgs e) 
        { 
            this.zoomSlider.ValueChanged -= //清除滑块控件的事件处理器  
                new RoutedPropertyChangedEventHandler(this.Slider_ValueChanged); 
            this.zoomSlider.Value = Math.Round( e.Scale * 100, 0);//更新滑块的值  
            this.zoomSlider.ValueChanged += //重新关联滑块的事件处理器  
                new RoutedPropertyChangedEventHandler(this.Slider_ValueChanged); 
        } 

///


  /// 当页面缩放后更新滑动条控件的位置
  ///

  private void SimplePageView_ZoomChanged
            (object sender, PageViewer.ZoomRoutedEventArgs e)
  {
   this.zoomSlider.ValueChanged -= //清除滑块控件的事件处理器
                new RoutedPropertyChangedEventHandler(this.Slider_ValueChanged);
   this.zoomSlider.Value = Math.Round( e.Scale * 100, 0);//更新滑块的值
   this.zoomSlider.ValueChanged += //重新关联滑块的事件处理器
                new RoutedPropertyChangedEventHandler(this.Slider_ValueChanged);
  }
在代码中,因为滑块的值变化后,会触发ValueChanged事件,而该事件又会设置PageViewer的Scale属性,这样会形成循环触发事件,

所以代码先去掉了对于ValueChanged事件的关联,而设置完值后再重新关联事件。

[csharp]
///

  
       /// 触发放大缩小路由事件  
       ///
  
       protected void RaiseZoomChanged() 
       {  //定义路由事件参数实例  
           ZoomRoutedEventArgs args = new ZoomRoutedEventArgs(_scale); 
           args.RoutedEvent = ZoomChangedEvent;//指定路由事件代码  
           RaiseEvent(args);//引发路由事件  
       } 

 ///


        /// 触发放大缩小路由事件
        ///

        protected void RaiseZoomChanged()
        {  //定义路由事件参数实例
            ZoomRoutedEventArgs args = new ZoomRoutedEventArgs(_scale);
            args.RoutedEvent = ZoomChangedEvent;//指定路由事件代码
            RaiseEvent(args);//引发路由事件
        }
 路由事件的定义:

路由事件和 . NET事件的定义有一些区别。路由事件的定义是由公共的静态RoutedEvent成员加一个约定的Event后缀组成,

路由事件需要在.NET事件系统中进行注册。

然后路由事件也有一个和普通.NET事件一样的事件定义,或者是一个事件包装器,使得可以像使用普通事件那样使用路由事件。

[csharp]
///

  
        /// 注册路由事件  
        ///
  
        public static readonly RoutedEvent  
            ZoomChangedEvent = EventManager.RegisterRoutedEvent("ZoomChangedEvent", 
                                                                RoutingStrategy.Bubble, 
                                                                typeof(ZoomChangedEventHandler), typeof(PageViewer)); 

///


  /// 注册路由事件
  ///

  public static readonly RoutedEvent
            ZoomChangedEvent = EventManager.RegisterRoutedEvent("ZoomChangedEvent",
                RoutingStrategy.Bubble,
                typeof(ZoomChangedEventHandler), typeof(PageViewer));

[csharp]
///

  
    /// 事件处理委托  
    ///
  
    public delegate void ZoomChangedEventHandler(object sender, ZoomRoutedEventArgs e); 
    ///   
    /// 路由事件的普通属性定义  
    ///
  
    public event ZoomChangedEventHandler ZoomChanged 
    { 
        add { AddHandler(ZoomChangedEvent, value); } 
        remove { RemoveHandler(ZoomChangedEvent, value); } 
    } 

 ///


  /// 事件处理委托
  ///

  public delegate void ZoomChangedEventHandler(object sender, ZoomRoutedEventArgs e);
  ///
  /// 路由事件的普通属性定义
  ///

  public event ZoomChangedEventHandler ZoomChanged
  {
   add { AddHandler(ZoomChangedEvent, value); }
   remove { RemoveHandler(ZoomChangedEvent, value); }
  }
在代码中,定义了一个ZoomChangedEventHandler类型的委托,首先调用定义一个名为ZoomChangedEvent的RoutedEvent,通过调用

EventManager.RegisterRoutedEvent()方法向WPF的事件系统注册路由事件。

RoutingStrategy枚举用于指定路由策略,路由策略是指事件在触发后,事件如何在元素树中传递的方式,有如下3种可选:

Bubble:冒泡传递,事件首先在源元素上触发,然后从每一个元素上沿着树传递,直到根元素为止;

Tunneling: 逐道传递,事件首先在根元素上被触发,依次向源元素传递。


 ============================================================================================================================

 

 4、6 处理屏幕滚动
当按下PageDown或向下方向键时,会调用ManageScroolDown()方向进行滚动,相反会调用ManageScroolUp()处理向上滚动。

这两个方法实现了屏幕滚动和翻页的操作。

 加载中...

[csharp]
//处理键盘事件  
    private void PageContent_PreviewKeyUp(object sender, KeyEventArgs e) 
    { 
        if (e.Key == Key.LeftShift)//如果用户控下左边的Shift键  
        {   //显示放大镜工具  
            Magnifier.Display(Visibility.Hidden); 
//释放页面事件铺获  
this.PageContent.ReleaseMouseCapture(); 
//己处理该预览事件,下面的元素不再处理  
e.Handled = true; 
            return; 
        } 
        //如果按下PageDown或向下方向键  
        if (e.Key == Key.PageDown || e.Key == Key.Down) 
        { 
            ManageScroolDown();//处理向下滚动  
            e.Handled = true; 
return; 
        } 
        //如果按下PageUp或向上方向键  
        else if (e.Key == Key.PageUp || e.Key == Key.Up) 
        { 
            ManageScroolUp();//处理向上滚动,并触发事件  
            e.Handled = true; 
return; 
        } 
    } 

    //处理键盘事件
        private void PageContent_PreviewKeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.LeftShift)//如果用户控下左边的Shift键
            {   //显示放大镜工具
                Magnifier.Display(Visibility.Hidden);
    //释放页面事件铺获
    this.PageContent.ReleaseMouseCapture();
    //己处理该预览事件,下面的元素不再处理
    e.Handled = true;
                return;
            }
            //如果按下PageDown或向下方向键
            if (e.Key == Key.PageDown || e.Key == Key.Down)
            {
                ManageScroolDown();//处理向下滚动
                e.Handled = true;
    return;
            }
            //如果按下PageUp或向上方向键
            else if (e.Key == Key.PageUp || e.Key == Key.Up)
            {
                ManageScroolUp();//处理向上滚动,并触发事件
                e.Handled = true;
    return;
            }
        }
 

[csharp]
//处理ScrollViewer向上滚动  
      private void ManageScroolDown() 
      { 
          try 
          {   //如果滚动条向上偏移量加上可视高度大于垂直大小  
              if (this.PageContent.VerticalOffset +  
                  this.PageContent.ViewportHeight >=  
                  this.PageContent.ExtentHeight) 
              {   //如果不用到页面底部  
                  if (!WaitAtBottom) 
                  {  //设置该属性的值  
                      WaitAtBottom = true; 
                      return; 
                  } 
                  else WaitAtBottom = false; 
                  //触发页面变更事件  
                  RaisePageChanged(1); 
        } 
          } 
          catch (Exception err) 
          {  //如果出现异常显示异常信息  
              ExceptionManagement.Manage("PageViewer:ManageScroolDown", err); 
          } 
      } 

  //处理ScrollViewer向上滚动
        private void ManageScroolDown()
        {
            try
            {   //如果滚动条向上偏移量加上可视高度大于垂直大小
                if (this.PageContent.VerticalOffset +
                    this.PageContent.ViewportHeight >=
                    this.PageContent.ExtentHeight)
                {   //如果不用到页面底部
                    if (!WaitAtBottom)
                    {  //设置该属性的值
                        WaitAtBottom = true;
                        return;
                    }
                    else WaitAtBottom = false;
                    //触发页面变更事件
                    RaisePageChanged(1);
    }
            }
            catch (Exception err)
            {  //如果出现异常显示异常信息
                ExceptionManagement.Manage("PageViewer:ManageScroolDown", err);
            }
        }
实际上ManageScroolDown并没有处理滚动,而是设置了滚动的状态后,将滚动工作交给了PageChanged事件。

MainWindow.maml.cs的PageChanged事件处理中,将根据传入的PageRoutedEvnetArgs参数来进行实际的滚动操作。

代码如下:

[csharp]
//处理滚动和页面变更  
    private void SimplePageView_PageChanged 
           (object sender, PageViewer.PageRoutedEventArgs e) 
    { 
        if (e.PageOffset == -1) //如果是向上跳转页面  
        { 
            if (_CurrentBook.GotoPreviousPage())//跳转到上一页面  
            {   //当前页面将为上一页面  
                this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage(); 
                   //指定主页面的文件路径信息  
                this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath; 
                   //滚动到上一页面的顶部  
                this.SimplePageView.ScrollToBottom(); 
            } 
        } 
        else 
            if (e.PageOffset == 1) //如果是要向下跳转页面  
            { 
                if (_CurrentBook.GotoNextPage())//跳到下一页  
                {    //显示下一页面  
                    this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage(); 
                       //指定下一页面的文件路径  
                    this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath; 
                       //滚动到页面顶部  
                    this.SimplePageView.ScrollToHome(); 
                } 
            } 
    } 

 //处理滚动和页面变更
  private void SimplePageView_PageChanged
            (object sender, PageViewer.PageRoutedEventArgs e)
  {
   if (e.PageOffset == -1) //如果是向上跳转页面
   {
    if (_CurrentBook.GotoPreviousPage())//跳转到上一页面
    {   //当前页面将为上一页面
     this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage();
                    //指定主页面的文件路径信息
     this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath;
                    //滚动到上一页面的顶部
     this.SimplePageView.ScrollToBottom();
    }
   }
   else
    if (e.PageOffset == 1) //如果是要向下跳转页面
    {
     if (_CurrentBook.GotoNextPage())//跳到下一页
     {    //显示下一页面
      this.SimplePageView.Source = _CurrentBook.GetCurrentPageImage();
                        //指定下一页面的文件路径
      this.PageInfo.Content = _CurrentBook.CurrentPage.FilePath;
                        //滚动到页面顶部
      this.SimplePageView.ScrollToHome();
     }
    }
  }

在代码中,根据PageRoutedEventArgs传入是否翻页值,调用_CurrentBook的GotoPreviousPage或GotoNextPage来进行上下翻页,

并调用ScrollToBootom或ScrollToHome滚动到页面的底部或顶部。

ScrollViewer控件本身能处理上下方向键进行上下滚动的工作,但是不理解向上或向下翻页的行为。通过处理

PageContent_PreviewKeyUp事件,在到达屏幕顶部或底部时可以进行上下翻页,大大提升阅读体验。

 

 4、7  控制鼠标滚轮
两种行为:

(1) 在按下Ctrl+鼠标滚轮,会对图书页面进行放大或缩小

(2) 如果不按下Ctrl,将进行屏幕滚动的工作。

ScrollViewer本身可以处理鼠标滚轮的动作,但是不理解上下翻页的行为,

因此可以为其添加翻页功能。

[csharp]
//处理鼠标滚轮事件  
      private void PageContent_PreviewMouseWheel 
          (object sender, MouseWheelEventArgs e) 
      { 
          //如果按下了键盘左边的Ctrl键  
          if (Keyboard.IsKeyDown(Key.LeftCtrl)) 
          {   //更新屏幕内容,进行大小缩放  
        UpdateContent(e.Delta > 0); 
              e.Handled = true; 
          } 
          else 
          { 
              if (e.Delta > 0)//如果是向上滚动  
              { 
                  ManageScroolUp();//向上翻页  
              } 
              else 
              { 
                  ManageScroolDown();//向下翻页  
              } 
          } 
      } 

  //处理鼠标滚轮事件
        private void PageContent_PreviewMouseWheel
            (object sender, MouseWheelEventArgs e)
        {
            //如果按下了键盘左边的Ctrl键
            if (Keyboard.IsKeyDown(Key.LeftCtrl))
            {   //更新屏幕内容,进行大小缩放
    UpdateContent(e.Delta > 0);
                e.Handled = true;
            }
            else
            {
                if (e.Delta > 0)//如果是向上滚动
                {
                    ManageScroolUp();//向上翻页
                }
                else
                {
                    ManageScroolDown();//向下翻页
                }
            }
        }
4、8 实现页面拖动效果
4、9 创建放大器用户控件
作者:chenyujing1234

你可能感兴趣的:(c#,阅读器)