日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > C# >内容正文

C#

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

發布時間:2024/3/13 C# 66 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C#入门学习-----图书阅读器(WPF 用户控件技术) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

歡迎大家提出意見,一起討論!

轉載請標明是引用于 http://blog.csdn.net/chenyujing1234

需要源碼請與我聯系。

?

?編譯平臺:VS2008 + .Net Framework 3.5

??????? 語言: C#

1、圖書閱讀器系統架構

1、2 系統架構設計

在這個系統中出現在的實體有圖書目錄、圖書列表、圖書、壓縮格式的圖書、圖像緩存等。

(1) 文件夾可以直接定義為一個類。因為該對象相對固定,不同的文件夾除了名稱唾位置不一樣外,還可能會有一些其他變化的特性。

(2)每個文件夾包含多部書。因為圖書的類型不是固定的,比如有壓縮文件類型的圖書和其他格式的圖書,需要抽象出來實現一個接口

(3) 每本書包含多個頁面。因為每個頁面的格式是不同的,因些也需要進行抽象。

(4) 每本圖書會包含一個圖像緩存,該緩存提供的功能相對固定,當然也可以進一步抽象。

Catalog代表一個文件夾類,它包含代表該目錄下所有圖書的ObservableCollection<IBook>泛型集合類

IBook是抽象出來的代表一部圖書的接口,它實現了INotifyPropertyChanged以便實現UI級別的綁定

BaseBook是一個實現了IBook接口的類,提供了對于每本圖書的基本實現。

RarBook通過派生自BaseBook類,實現了壓縮格式的圖書對象

IBookItem接口是代表圖書書頁的接口,IBook接口包含一個類型為List<IBookItem>泛型集合,來表示一本書的所有圖書頁

RarPage實現了IBookItem接口,提供了對于RarBook類型圖書的書頁實現。

?

1、3 項目文件夾介紹

在此圖中

Dependencies文件夾包含了項目中使用到的第三方類庫或程序,比如pdftohtml.exe用于將pdf文件轉換為Html格式。

????????????????????????? SevenZipSharp.dll用于壓縮或解壓縮文件,使用的時候需要7z.dll來進行壓縮或解壓縮。

???????????????????????? WPFToolkit.dll包含一些額外的控件來豐富WPF控件。

項目根目錄下的app.config是應用程序配置文件。

?

2、系統核心類的實現

這一節將介紹如何實現。主要內容涵蓋了.NET的反射、多線程、操作文件和文件夾知識,以及如何使用面向對象方式設計和實現類

2、1? 實現圖書目錄Catalog類

圖書閱讀器每次在啟動時,會根據在選項指定的文件路徑異步加載圖書到ListBox以顯示書籍。

或者用戶單擊“打開”按鈕,從彈出的打開文件窗口中選擇一個文件。

Catalog會將該文件加載到其圖書列表中,Catalog類要能從文件夾中枚舉圖書文件,也要能從特定文件中加載圖書。

從圖可以看出,Catalog包含實現了IBook接口的實例列表,作為其內含的圖書。

因為該類不被設計用于繼承或開,因此將該類指定為Internal.

internal class Catalog


Catalog類定義了3個屬性,分別用于指定文件路徑、用于保存圖書的列表及一個布爾值獲取和設定圖書變更信息

圖書列表采用泛型集合,原因是因為它采用了集合通知。

#region -----------------屬性區域-----------------private string _bookPath = string.Empty;//文件路徑public string BookPath //文件路徑屬性{get { return _bookPath; }//返回值set { _bookPath = value; }//設置值}private ObservableCollection<IBook> _Books =//圖書集合new ObservableCollection<IBook>();public ObservableCollection<IBook> 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帶參數的重載方法實現如下:

public void Load(string path)//該重載方法傳入一個文件路徑{try{_bookPath = path;//將路徑指定給BookPath屬性Load(); //調用不帶參數的重載的Load方法}catch (Exception err) //在加載過程出現錯誤則觸發異常{ //調用定制的異常管理窗口顯示異常信息ExceptionManagement.Manage("Catalog:LoadPath", err);}}


代碼內部調用了Load另一個重載,如果產生異常,則會產生定制的ExceptioMamagerment類的Mange()方法來產生一個異常窗口。

Load() 方法實現了加載的所有核心邏輯。

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()方法將從保存的二進制文件中恢復歷史記錄

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<IBook>();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中。

因為封面資料被保存到另一個二進制文件中,也需要使用反序列化從流中加載信息。

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()方法在一個后臺

線路中遞歸文件夾,得到書簽信息。

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遞歸解析

傳入的文件夾路徑。該方法實現了重獲書簽信息的核心邏輯

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<IBook>類,該類將要與UI進行綁定來自動更新UI,

而IBook實現了INotifyPropertyChanged接口。同樣地,在一本書的屬性信息變化時觸發UI的變更,對于Books集合的

增刪改必須要與UI處于同一線程,因此使用了Dispatcher 的Invoke()方法。

2、5 保存圖書信息

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()方法。

// 從目錄中加載圖書列表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);}}


?

internal void RefreshThread(object o)//刷新圖書列表{try{ //首先,刷新己經不存在的圖書 // List也為泛型類型List<IBook> temp = new List<IBook>();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<IBook>泛型列表,循環Books集合,判斷指定的圖書對應的文件是否存在,如果不存在則加入到List<IBook>中準備移除,

然后在UI線程中進行循環移除

然后調用ParseDirectoryRecursiveWithCheck()方法,該方法與ParseDirectoryRecursive類似,是一個遞歸方法,該方法主要不同的是調用了

BookExist() 方法來判斷圖書的文件路徑與當前的文件路徑是否一致。

2、7? 定義圖書接口IBook

BookReader當前支持的圖書類型有限,僅RarBook這一類,但是系統在最初架構時,已經提供了彈性方式允許將來擴充

多種圖書文件格式,其BaseBook實現了IBook接口,開發人員可以通過派生自BaseBook類來實現多種格式圖書類

?

BaseBook實現了IBook接口,IBook接口又被Catelog引用,使用這種基于接口的方法可以實現程序間的解耦,

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

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類與其他類的關系如下:

RarBook從BaseBook基類派生,提供了對于Rar格式圖書的實現。BaseBook包括ImageCache圖像緩存。

BaseBook的Pages包含實現了IBookItem接口的對象集合,CurrentPage用于顯示當前的圖書頁面。

該類重載了構造函數,提供了一個接收文件路徑的構造函數,當文件路徑發生變化時,會觸發INotifyPropertyChanged接口

中定義的變更通知。

public BaseBook(string filePath){_filePath = filePath; //得到文件路徑RaisePropetyChanged("FilePath");//觸發文件路徑變更通知RaisePropetyChanged("FileName");//觸發文件變更通知}


下面從3個方面介紹BaseBook實現的功能:

(1) 實現書簽功能: BaseBook允許用戶定義或跳轉到書簽,提供書簽列表功能。

public void SetMark(){ //將當前頁面的路徑賦給BookmarkBookmark = _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提供上一頁、下一頁或定義到指定頁。

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的定義

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() 方法內部,

該方法在主窗體加載時會被調用。

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()方法來關閉窗體,實現如下:

//標題欄的關閉按鈕事件處理代碼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的方法來實現,代碼如下:

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 打開圖書按鈕
///打開一個不在當前文件夾的外部文件 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控件中。

//加載一部指定的圖書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 選項按鈕
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 全屏按鈕
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 實現上下文菜單事件處理

?

?

添加標簽、定位到標簽和移除標簽菜單項的實現代碼如下:

?

//將當前圖書的當前頁面設置為書簽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的內部行為。

//指定自動縮放類型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控件設置縮放變換;

/// <summary>/// 更新圖像控件的縮放,并觸發事件/// </summary>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的代碼如下:

/// <summary>/// 當頁面縮放后更新滑動條控件的位置/// </summary>private void SimplePageView_ZoomChanged(object sender, PageViewer.ZoomRoutedEventArgs e){this.zoomSlider.ValueChanged -= //清除滑塊控件的事件處理器new RoutedPropertyChangedEventHandler<double>(this.Slider_ValueChanged);this.zoomSlider.Value = Math.Round( e.Scale * 100, 0);//更新滑塊的值this.zoomSlider.ValueChanged += //重新關聯滑塊的事件處理器new RoutedPropertyChangedEventHandler<double>(this.Slider_ValueChanged);}


在代碼中,因為滑塊的值變化后,會觸發ValueChanged事件,而該事件又會設置PageViewer的Scale屬性,這樣會形成循環觸發事件,

所以代碼先去掉了對于ValueChanged事件的關聯,而設置完值后再重新關聯事件。

/// <summary>/// 觸發放大縮小路由事件/// </summary>protected void RaiseZoomChanged(){ //定義路由事件參數實例ZoomRoutedEventArgs args = new ZoomRoutedEventArgs(_scale);args.RoutedEvent = ZoomChangedEvent;//指定路由事件代碼RaiseEvent(args);//引發路由事件}


?路由事件的定義:

路由事件和 . NET事件的定義有一些區別。路由事件的定義是由公共的靜態RoutedEvent成員加一個約定的Event后綴組成,

路由事件需要在.NET事件系統中進行注冊。

然后路由事件也有一個和普通.NET事件一樣的事件定義,或者是一個事件包裝器,使得可以像使用普通事件那樣使用路由事件。

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

?

/// <summary>/// 事件處理委托/// </summary>public delegate void ZoomChangedEventHandler(object sender, ZoomRoutedEventArgs e);/// <summary>/// 路由事件的普通屬性定義/// </summary>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()處理向上滾動。

這兩個方法實現了屏幕滾動和翻頁的操作。

//處理鍵盤事件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;}}


?

//處理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參數來進行實際的滾動操作。

代碼如下:

//處理滾動和頁面變更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();}}elseif (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本身可以處理鼠標滾輪的動作,但是不理解上下翻頁的行為

因此可以為其添加翻頁功能。

//處理鼠標滾輪事件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 創建放大器用戶控件

?

總結

以上是生活随笔為你收集整理的C#入门学习-----图书阅读器(WPF 用户控件技术)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。