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

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

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

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

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com