位置:首页 » 技术 » C#入门学习-----图书阅读器(WPF 用户控件技术)

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

日期:2012-07-01 阅读:1num
Advertisement

编译平台:VS2008 + .Net Framework 3.5

语言: C#

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

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

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

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

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

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

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

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

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

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

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

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

1、3 项目文件夹介绍

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

在此图中

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

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

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

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

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

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

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

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

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

从图可以看出,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<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

#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带参数的重载方法实现如下:

[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<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; //返回结果
}

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中。

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

[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<IBook>类,该类将要与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<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);
});
}
}

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引用,使用这种基于接口的方法可以实现程序间的解耦,

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

[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 用户控件技术)

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。

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

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

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

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

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

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

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

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

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

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

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

DynamicResource进行设定。

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

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

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

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

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

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

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

每当加载图书目录时,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通过下图指定)

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

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

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

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

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

(1)ListBox的整体数据模板

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

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

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

(3)BaseBook的数据模板

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

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

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

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

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

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

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

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

[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 实现上下文菜单事件处理

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

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

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

[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控件需要处理和种鼠标和键盘事件来实现图书阅读器效果。

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

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属性根据用户在选项面板中的设置来指定自动缩放的类型;

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

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

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

[csharp]
/// <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();
}

/// <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事件,代码如下:

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

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

SimplePageView_ZoomChanged的代码如下:

[csharp]
/// <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);
}

/// <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事件的关联,而设置完值后再重新关联事件。

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

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

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

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

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

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

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

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

/// <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()处理向上滚动。

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

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

[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#入门学习-----图书阅读器(WPF 用户控件技术) C#入门学习-----图书阅读器(WPF 用户控件技术)

    编译平台:VS2008 + .Net Framework 3.5 语言: C# 1.图书阅读器系统架构 1.2 系统架构设计 在这个系统中出现在的实体有图书目录.图书列表.图书.压缩格式的图书.图像缓存等. (1) 文件夹可以直接定义为一个类.因为该对象相对固定,不同的文件夹除了名称唾位置不一样外,还可能会有一些其他变化的特性. (2)每个文件夹包含多部书.因为图书的类型不是固定的,比如有压缩文件类型的图书和其他格式的图书,需要抽象出来实现一个接口. (3) 每本书包含多个页面.因为每个页面的格

  • WPF用户控件中的按钮事件里,怎么取得其所在的Window窗体

    WPF用户控件中的按钮事件里,如何取得其所在的Window窗体? 如T, 谢谢! ------解决方案-------------------- 可以参照我博客里面的http://blog.csdn.net/fallincloud/article/details/6960255 FindParent的方法,用法Window w = FindParent<Window>(this); ------解决方案-------------------- 或者使用App.Current.MainWindow

  • 高手一个WPF用户控件的有关问题

    请教各位高手一个WPF用户控件的问题 我在VS2010下写了一个用户控件,内容主要是8个TextBox,并且将这个控件作为DataGrid的Cell,现在的问题是我使用 List<ganttLine> _data = new List<ganttLine>(); _data.Add(new ganttLine()); _data.Add(new ganttLine()); _dataX[0].percentage0 = 0.2f; _dataX[0].percentage1 = 0

  • .net WinForm用户控件开发-(一)带按钮的textbox控件 .net WinForm用户控件开发-(一)带按钮的textbox控件

    .net WinForm用户控件开发--(1)带按钮的textbox控件 今天大家一同学习下.net winform中用户控件的开发,这一节给大家演示一个带下划线和按钮的textbox控件,我们先来看下效果图吧! 下面我们来演示下怎么实现这样一个用户控件 1.创建用户控件 通过添加新项--用户控件 2. 在用户控件上拖放一个label,一个textbox,一个按钮 并设置控件的属性如下: label:设置lable的width:260,height:1 backcolor为黑色,这样label就

  • .net WinForm用户控件开发--(1)带按钮的textbox控件 .net WinForm用户控件开发--(1)带按钮的textbox控件

    今天大家一同学习下.net winform中用户控件的开发,这一节给大家演示一个带下划线和按钮的textbox控件,我们先来看下效果图吧! 下面我们来演示下怎么实现这样一个用户控件 1.创建用户控件 通过添加新项--用户控件 2. 在用户控件上拖放一个label,一个textbox,一个按钮 并设置控件的属性如下: label:设置lable的width:260,height:1 backcolor为黑色,这样label就变成了一条黑色下划线 textbox:设置boderstyle的属性为no

  • 快乐阅读   图书阅读器 快乐阅读 图书阅读器

    aTextReader图书阅读器可以阅读本地文本文件和在线小说,是一款基于.NET Framework 2.0 平台的图书阅读器.无需安装,点击运行.如果不能运行,请先安装.NET Framework 2.0.支持全屏阅读,也可定制阅读窗口大小.字体及背景图片等.1.2版可以阅读以下网站的在线小说(不支持收费章节的阅读):新浪读书.搜狐读书. 腾讯读书.小说阅读网.起点中文网.3263.net等. 首页菜单说明: 1.阅读:阅读本地的小文本文件,直接打开文件:选择"阅读页面主题"进行阅

  • MongoDB快速入门学习笔记七 MongoDB的用户管理操作

    MongoDB快速入门学习笔记7 MongoDB的用户管理操作 1.修改启动MongoDB时要求用户验证 加参数 --auth 即可. 现在我们把MongoDB服务删除,再重新添加服务 mongod --dbpath "D:\work\MongoDB\data" --logpath "D:\work\MongoDB\log\mongodb.log" --install --serviceName "MongoDB" --auth 2.创建用户,并

  • WPF怎么获取用户控件中控件的值

    WPF如何获取用户控件中控件的值 我的用户控件是由多个TEXTBOX控件组成的,现在想在程序中获取和给TEXTBOX分别赋值,该怎么做 ------解决方案-------------------- 探讨 引用: usercontrol1.textbox1.Text="111"; 这样找不到textbox1,就是这里纠结.

  • (最新)Windows 八 Metro 应用开发的入门 Metro App的几个新控件 (最新)Windows 八 Metro 应用开发的入门 Metro App的几个新控件

    (最新)Windows 8 Metro 应用开发的入门 Metro App的几个新控件 摘 要 基于Silverlight开发Metro App可以使用Silverlight原生的控件,为了更好的开发Metro App,控件库又专门增加了几个新的控件,如:GridView.Flipview.ProgressRing.SemanticZoom和VariableSizedWrapGrid等,这些控件为平板设备提供了良好的触控体验,这一章我们来介绍一下这几个控件的简单用法. 第1节 GridView

  • Asp.Net其他页面如何调用Web用户控件写的分页

    在要添加分页的页面加载时添加以下代码:(以图书分类为例) . 代码如下: Paging p = Paging1; //Web用户控件的ID p.DataControl = gvBookType; //要绑定数据的控件(此处是GridView) p.TableName = "BookShop_BookType"; p.Sort = "asc"; p.Column = "BookType_ID";

  • Web.config中注册用户控件和自定义控件

    在ASP.NET 的早先版本里,我们通过在页面的顶部添加 <%@ Register %> 指令来引入和使用自定义服务器控件和用户控件时,象这样: <%@ Register TagPrefix="scott" TagName="header" Src="Controls/Header.ascx" %> <%@ Register TagPrefix="scott" TagName="foot

  • Android开发学习 之 5、基本界面控件-5进度条 Android开发学习 之 5、基本界面控件-5进度条

    Android开发学习 之 五.基本界面控件-5进度条 五.基本界面控件-5进度条 5.5 进度条 5.5.1 ProgressBar 图5.5.1 ProgressBar android.widget. ProgressBar,继承自android.view.View .在android.widget包中.对应对话框ProgressDialog. ProgressBar有两种展示方式,表盘形式(普通.小.大)和条形填充形式.在layout定义时,需要通过设施style属性类设置展示方式. 常用

  • 用户控件既可以拖动,又可以点击解决办法

    用户控件既可以拖动,又可以点击 先说解决掉的问题吧! 一个Image既允许拖动又允许点击!在MouseDown中记录当前鼠标位置,在MouseUp中判断位置是否改变,若改变则是移动,若不改变则是点击. 现在的问题是我们很多东西都做成用户控件了,例如用户控件中有Image,又有Label.这时候如果要在主窗体上实现用户控件的拖放和点击,就得对用户控件以及其中的每个点击事件做出判断(判断是拖动还是点击.) 有没有好的方法解决这个问题? ------解决方案-------------------- W

  • 初学者请问:在用户控件中如何定义全局变量或是引用其它窗体的变量

    菜鸟请教:在用户控件中怎么定义全局变量或是引用其它窗体的变量? 学习例子中有个用户控件和模块,我想在窗体中得到用户控件中的一些参数,请教如何才能在窗体中使用用户控件中定义的变量? 分享到: ------解决方案-------------------- '以下是类或控件中增加属性的示例,即在其它引用的此类或控件的窗体\类\控件中对数据进行读取值'以下示例在其它窗体\模块中对引用了此类的数据Data进行操作''增加一个类,类名 Class1 , 以下代码在类中 Private Data As Str

  • 怎么给用户控件的孩子控件设置style

    如何给用户控件的孩子控件设置style? 我写了一个简单的用户控件 <UserControl x:Class="WpfApplication1.UserControl1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="

  • iPhone开发入门(八)- 程序画面与控件调整 iPhone开发入门(八)- 程序画面与控件调整

    iPhone开发入门(8)--- 程序画面与控件调整 From:http://www.2cto.com/kf/201110/107936.html 画面的构成 Cocoa Touch编程中,一个应用程序里面可以包含多个画面.通过列表选择来显示,或者通过下方的标签来显示,等等.CocoaTouch中将这样一个一个的画面成为 View. 如果只是创建一个View,不能实现一个完整的应用程序.这里,必须生成 Outlet 和 Action,这样才能将程序与 View 连接起来.比如取得View中配置控

  • .net WinForm用户控件开发-(五)用户控件复杂属性设置 .net WinForm用户控件开发-(五)用户控件复杂属性设置

    .net WinForm用户控件开发--(5)用户控件复杂属性设置 这一节,大家共同学习下,用户控件的自定义的复杂的属性设置,我们这里自定义一个用户控件和自定义一个属性. 本节重点: 1.怎样定义复杂属性 2.复杂属性和基本类型相互转换 1.第一步, 先来自定义一个类,代码如下 /// <summary> /// 自定义属性类 /// </summary> public class CustomAttri { public CustomAttri(int width, int he

  • .net WinForm用户控件开发-(四)用户控件的基本属性设置

    .net WinForm用户控件开发--(4)用户控件的基本属性设置 在前几节中,我们演示了怎样进行用户控件开发,大家可能对用户控件中的一些属性的基本设置还存在疑惑,这节我们共同学习下用户控件的基本属性设置. 所谓基本属性设置,就是我们常常看到控件在属性设计器中可以对属性进行设置,有些属性在设计中具有默认值,有些在属性设计器中会弹出一个编辑器,供用户选择值. 这节内容 1.简单数据类型的属性设置 2.复杂数据类型的属性设置 1.简单数据类型的属性设置 简单数据类型,就是.NET中常见的类型,例如

  • 用户控件添加子用户控件后捕杀不到事件

    用户控件添加子用户控件后捕捉不到事件 我用WPF自己封装了一个用户控件A,A中包含一个stackpanel,然后像A的stackpanel中添加用户控件B,B中写了鼠标左键事件,但是我现在把B家族到A中后,B就捕捉不到鼠标左键事件了,B是通过代码动态的添加进去的.各位兄弟,没分了,只能给20了 --cut-- B是通过代码动态的添加进去 -> 可以在 B中后台 自定义事件,触发时上抛 出来. 在添加B的代码里 触发.

  • Android学习笔记一:使用WebView控件浏览网页 Android学习笔记一:使用WebView控件浏览网页

    Android学习笔记1:使用WebView控件浏览网页 在Android中,可以使用Webview控件来浏览网页.通过使用该控件,我们可以自制一个简单的浏览器,运行效果如图1所示. 图1 运行效果 1.WebView 在使用WebView控件时,首先需要在xml布局文件中定义一个WebView控件,定义的方法如下: 1 <WebView 2 android:id="@+id/webView" 3 android:layout_width="match_parent&q

最新文章
  • 松下假冒 SSD 流出

    http://news.smzdm.com/p/22423 近日,本公司的固态硬盘(以下简称 SSD )假冒品被发现在部分电商网站.市场流通.上述假冒品,是将本公司的注册商标( Panasonic )粘贴在并非由本公司设计.生产的 SSD 上而成,性能.品质方面也与正品不一样." 其中涉及假冒的产品包括:" RP-V2M , V3M , X3M , XTM , XCM 等,都并非正品型号.(正品型号应为 RP- SSB120 . SSB240 ) --cut-- abelyao在201

  • SpaceX猎鹰9号v1.1火箭今夜发射,直播地址奉上,Paypal和特斯拉创办者的传奇公司要逆天了

    http://new.livestream.com/spacex/F9-6 --cut-- inee在2013-09-29 23:40:5回答到: 直播板凳 inee在2013-09-29 23:41:3回答到: 二楼 贩卖花生 恰恰香瓜子 可乐 农夫山泉 望远镜 panlilu在2013-09-29 23:44:3回答到: zh panlilu在2013-09-29 23:44:5回答到: 上调刚打了俩字 panlilu在2013-09-29 23:46:2回答到: 横屏真容易误操作..上面错

  • 准爸妈优生宝宝的注意事项 准爸妈优生宝宝的注意事项

    1.消除影响受孕的不良情绪及心理障碍 女性在情绪波动时受孕,不容易形成正常的受精卵,可能导致胚胎发育迟缓,或导致胎儿体重轻.早产甚至畸形.因此,准备受孕时如果情绪波动,心情不愉快,最好先进行心理调整,推迟受孕时间.心理障碍也是一种疾病,同样会影响受孕,如导致不孕.长期不孕特别是经过多方治疗效果不佳的女性,反过来会使心理压力更加沉重,如对人际关系敏感.焦虑.抑郁.偏执等. 在准备怀孕时要注意消除自己的不良情绪.一旦产生心理障碍,积极进行心理疏导或心理治疗.同时,注意提高自身"免疫力",即

  • PHP提示:Fatal error: Allowed memory size of

    出现"Fatal error: Allowed memory size of 8388608 bytes exhausted (tried to allocate 775920 bytes)"这种错误,首先要先检查你的php程序是否出现了死循环. 解决办法(三种): 一.修改php.ini(推荐) memory_limit = 12M 二.在程序里面添加如下语句 ini_set('memory_limit', '12M'); 三.在根目录建立 .htaccess文件,添加如下内容 ph

  • Java程序多线程递归弥补管理漏洞

    Java多线程递归在我们使用的时候需要我们不断的进行学习,其实每个语言都可以在源代码中找到问题的解决方案.当每个迭代彼此独立,并且完成Java多线程递归中每个迭代的工作,意义都足够重大,足以弥补管理一个新任务的开销时,这个顺序循环是适合并行化的. 1.public voidParallelRecursive(final Executorexec, List 2.for(Node n:nodes){ 3.exec.execute(new Runnable(){ 4.public void run(

  • 波拉波拉岛旅游攻略 波拉波拉岛旅游攻略

    波拉波拉距离大溪地岛 268公里,只有10公里长,4公里宽,环岛一周32公里,由一个主岛与周围环礁所组成,主岛与环礁间拥有大片的清澈碧绿的泻湖,是不可多得的潜水胜地.岛上一共有18家酒店,到各个酒店都要打水上TAXI,当快艇飞驰在碧绿色的水面上,海风扑面而来,却没有一丝丝海水的腥气,你会觉得一切都在梦幻中.这种短时的头脑空白也会发生在清晨,在半梦半醒之中,推开阳台门,看到如此美妙的碧海蓝天,有种不知身在何处的感觉. 波拉波拉也是与名人遭遇率最高的海岛之一.当我们在四季酒店的餐厅吃早餐时,身边的客

  • Windows 8系统下如何更改默认弹出的浏览器方式图解 Windows 8系统下如何更改默认弹出的浏览器方式图解

    Windows 8操作系统下,配备了IE浏览器家族中最新的IE10浏览器,但与以往的Windows版本有所不同,Windows 8配备了传统界面与Metro界面两种风格,同样IE10在两个界面也是不同的,传统界面的IE10变化不多,而Metro 风格界面中弹出的IE10界面却变化很大 如图所示: 想要设置在打开网页时,默认弹出更符合自己使用习惯的浏览器,请按照以下方法操作: 1.在传统页面双击[IE浏览器]. 2. 点击[设置]中的[Internet选项]. 3. 点击[程序]选项卡,然后点击"

  • 每天刮腿20分钟瘦掉萝卜腿,瘦腿小妙招瘦腿更轻松 每天刮腿20分钟瘦掉萝卜腿,瘦腿小妙招瘦腿更轻松

    腿粗就显得人特别的"魁"特别的胖,穿衣就不好看,无论是夏季穿裙子还是冬季穿打底裤,都会特别的明显,那要如何快速瘦腿呢?今天小编就带你去看看每天刮腿20分钟瘦掉萝卜腿,瘦腿小妙招瘦腿更轻松. 本站阅读配图 每天刮腿20分钟瘦掉萝卜腿 瘦腿小妙招瘦腿更轻松 每天刮腿20分钟买一只牛角的刮痧板,腿上抹好乳液,按照穴位从上向下刮,每个穴位刮20下,要快速.用力,直到刮出红道道,左腿完了换右腿.最好是每天晚上睡觉之前刮,刮完后不要接触冷水,直接睡觉. 垫脚尖无处不在 垫脚尖时在脚尖与地面的接触点

  • pb控制键盘输入字符串解决办法

    pb控制键盘输入字符串 问题是这样的: 1.有个单行编辑框,可以在此输入字符串. 2.当编辑框中有字符时,我按键盘上的功能键F3,pb程序就会模拟我们打字那样,将编辑框中的字符串输出. ------解决方案-------------------- 新的源码 http://download.csdn.net/detail/yyoinge/3893984

  • 10月16日是什么节日

    问:10月16日是什么节日? 答:世界粮食日,是在1979年11月第20届联合国粮农组织大会上通过的,其目的是为了唤起全世界对发展粮食和农业生产的高度重视. 大事记 690年:武则天登上神都太初宫正门:则天门的门楼,宣布改"唐"为"周",成为中国历史上被广泛认可的唯一的女皇帝. 1813年:拿破仑·波拿巴率领法国军队在德国莱比锡与第六次反法同盟作战,莱比锡战役爆发. 1916年:计划生育组织的创立者美国妇女玛格丽特·桑格在纽约州布鲁克林开设了第一家计划生育门诊所.

热门推荐
  • 【独家】专访蓝汛孙凯:积17年CDN经验聚焦视频 【独家】专访蓝汛孙凯:积17年CDN经验聚焦视频 [流媒体网]消息:北京蓝汛通信技术有限责任公司新媒体产品部总经理孙凯在流媒体网深圳论道上发表了题为<CDN加速"大""小""移"三屏交互时代>的演讲,他说2005到2015年,是在线娱乐发展的黄金十年,2015年中国在线娱乐市场规模超过2000亿元,相当于7个NBA的总收入. 孙凯表示,影响在线体验的主要有卡顿.带宽成本.多屏互动.宽带运营四个方面,蓝汛对症下药,推出云视频解决方案.那么,蓝汛CDN有何特别之处?CDN和云啥关系?三屏
  • 老中医推荐11个治疗咳嗽小偏方 老中医推荐11个治疗咳嗽小偏方 1.白糖拌鸡蛋治疗咳嗽; 2.木耳蒸鸭蛋; 3.核桃.芝麻.生姜粉治疗咳嗽; 本站阅读配图 4.梨汁炖冬菇治疗咳嗽; 5.吃生姜片也止咳; 6.煮萝卜水可治疗咳嗽; 7.南瓜藤汁止咳; 8.杏仁治疗咳嗽; 9.搽风油精止咳平喘; 10.罗汉果泡水治疗咳嗽; 11.冰糖蒸白果.
  • 关于首页焦点图的色彩搭配——黑色 关于首页焦点图的色彩搭配——黑色 做电商的都知道装修的重要性,我往往把网店和女人拿来做比较,店铺的装修好比女人的脸,能否吸引你.留住你,外表永远是第一要素.但是,网店装修总 是很让人头疼,色彩如何搭配经常让人困扰,很难驾驭.所以我们必须找到适合我们品牌和定位的色系来表达我们产品,做好色彩搭配,尽量使用首页的焦点图来吸 引用户.留住用户.所以我今天想和大家分享的便是关于首页焦点图的色彩搭配--黑色,先从黑色开始说起吧. 1.黑色的意义 不同的颜色代表的含义不同,色彩也是具有性格.寓意和情感的.而在我国,黑色一般代表了这些性格:成熟
  • 芒果TV七月力推“同步剧+轻应用”模式 演绎全新视娱体验 芒果TV七月力推“同步剧+轻应用”模式 演绎全新视娱体验 自芒果TV推出"马栏坡的夏天"暑期内容专题以来,其品类多样的独播视频与创新策划内容形成精彩的内容组合拳,深获用户喜爱.七月,芒果TV将继续从综艺节目.电视剧等内容资源层面发力,并融入全新的轻应用互娱模式,为用户带来更加清凉的视娱体验. 好看:11档同步剧+2部电影缤纷放送 独家综艺策划新鲜有料 正在芒果TV电视剧频道同步跟播的11部暑期大戏,涵盖各种类型,其中青春偶像剧5部:<不一样的美男子>.<A咖的路>.<嫁给爱情>.<恋恋不忘>.&
  • Word快速输入大写金额方法 Word快速输入大写金额方法 1.打开word文档,选择"插入"选项卡,单击"符号"选项组中的"编号"按钮. 2.弹出"编号"对话框,在"编号"文档框中输入我们需要转换成中文大写金额的阿拉伯数字金额,在"编号类型"文本框中选择"壹,贰,叁-",点击"确定"即可. 3.可以看到刚刚输入的阿拉伯数字金额,都变成了中文大写数字金额.
  • qq炫舞第78关挥洒汗水SSS怎么搭配?炫舞挥洒汗水SSS搭配图 qq炫舞第78关挥洒汗水SSS怎么搭配?炫舞挥洒汗水SSS搭配图 qq炫舞第78关挥洒汗水SSS怎么搭配呢,这个是设计师生涯中的一个关卡了,如果你想快速通过此关我们来可进入参考下面搭配高分图了,希望对大家有帮助. qq炫舞设计师生涯第78关挥洒汗水SSS搭配 得分:40785 搭配分:18942 设计加成:16620 集卡度:5218 奖励:5个 搭配点评:女生带上没子与耳机让我们有时尚潮人的味道,然后一般简装休闲的运动装非常适合挥洒汗水SSS搭配了.
  • 京东商城刷积分(已修复) 京东商城刷积分(已修复) 新闻报道:http://www.2cto.com/News/201210/164987.html 京东商城购物完成后,评价大于100元的商品,可以获得2积分.但订单没有判断是否是自己的订单.可遍历订单号.刷积分. POST /JdVote/TradeComment.aspx?ruleid=订单ID HTTP/1.1 Host: market.360buy.com Connection: keep-alive Content-Length: 2171 Cache-Control: max-age=