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);}}
?
代碼首先實例化一個新的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;}}
?
實際上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 用户控件技术)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unity Live2D SDK的使用方
- 下一篇: C# C++ 互操作:C++向C#输出不