C.B.R.






4.96/5 (52投票s)
漫画和电子出版物阅读器,具有图书馆管理、扩展文件转换和设备支持功能。
引言
鉴于第一个项目的“成功”和一些空闲时间,我在同一个CodePlex项目上启动了一个全新的版本。
我决定重写它,以便更深入地探索MVVM模式,并将其扩展到其他电子书格式。我也对7款手机的开发很感兴趣,所以我采用了我在iPhone应用程序上看到的动态书籍的概念。路线图包括:
- 更好的用户界面和设计:Ribbon...
- 多种格式支持和转换:图片、PDF、XPS、CBR/RAR、CBZ/ZIP、ePUB...
- 动态书籍格式:CBZD,一种完整的zip格式,包含与页面关联的附加XML文件,这些文件包含框架描述。CBR包含一个设计器,用于“编辑”书籍,以便在不同的页面区域添加带有顺序和时间安排的框架。这使得桌面上的自动阅读成为可能,但主要用于手机应用程序。
- 支持外部设备和Windows Phone 7应用程序。
项目内容
作为开发人员,除了这里没有描述的许多技巧之外,您可以在这个项目中找到有用的内容:
- 新功能:OPDS feed、按代码本地化功能、MVVM设计中的多文档和工具箱、为动态书籍重新设计的MVVM框架编辑器、新的平板电脑和7款手机模拟器、‘任意CPU’并兼容32位和64位平台、用于PDF转换的图像合并器等...
- 基于Mitsu Futura“书籍”的两页书籍视图
- 本地化引擎,可与resx、xml、bin配合使用,并可根据您的需求进行扩展...!
- 一个带有网格、简单和复杂缩略图视图的浏览器视图
- 一个显示直接操作和“xml新闻”feed的起始主页
- 相当复杂的MVVM模式
- MVVM中简单的类似浏览器视图,带树形和文件夹视图
- 使用WMI进行USB设备检测
- PDF图像提取
- SevenZipSharp:提取、压缩、内存使用
- Office风格:Ribbon控件使用,类似最近文件列表的后台设计
- 控件:放大镜、5星评级、分割器扩展器、WaitSpin、ZoomFlyer、扩展ListView
- XPS读写,ePUB解析和转换
- 单实例、参数处理和文件类型注册
章节
截图
您可以在产品网站上找到更多截图:http://guillaume.waser.free.fr
代码:架构和原则
由于项目变得比最初复杂一些,并且一直处于测试阶段,我将文章分为几个“高级”部分:核心类、MVVM模式类和编码技巧……直到架构稳定下来,才能给出更完整的描述。
核心模型类
下面说明的核心模型类包含一个表示库的Catalog
类。它映射到ExplorerViewModel
,并由相应的CatalogService
管理。所有书籍都由一个Book
实体表示,该实体(如果可能)扩展到Pages
。Zone
类专用于我的动态书籍新格式。当需要额外数据时,它存储在Book
的Tag
属性中(例如ePUB格式)。
核心服务类
以下是主要服务类。FileService
和FileExtension
管理我们需要将文件(基于扩展名)与对话框过滤器等数据关联起来的数据,以及关联的模型、服务和视图模型。这将被BookServiceFactory
用来创建相应的ViewModel
,该ViewModel
也将使用反射创建相应的服务。BookInfoService
是一个独立的类,仅管理Book
二进制内部结构的加载/保存操作。CatalogService
是管理库的类。
更新...!
Fileservice
和FileInfo
类已被DocumentFactory
、DocumentInfo
和DocumentType
取代,这些类链接到核心模型。
注意:关于内部文件结构...C.B.R.不使用任何数据库。它是一个独立的二进制文件,您可以将其存储在您想要的任何位置,该文件集中了目录信息(指向书籍文件的路径)。在当前应用程序文件夹下,我将书籍信息(如评分或封面)存储在一个单独的bin文件中,以便它们可以在多个库之间共享。您可能会丢失您的库,但如果书籍二进制文件在那里,那么什么都不会丢失!
核心工作区类
这些类管理工作环境中的所有数据。单例类WorkspaceService
将设置存储在WorkspaceInfo
类中,该类包含程序选项(用户可以在后台更改)、两个RecentFileInfo
列表(用于书籍和库的最近文件列表)以及作为DeviceInfo
集合的支持设备列表。设置类通过程序属性进行序列化。它在程序启动时加载,并在关闭时自动保存。
更新...!
ExtendedInfo
包含扩展选项的数据。FeedInfo
和FeedItemInfo
是后台视图FeedInfoView
的数据。已添加ProxyInfo
类来管理任何特殊的互联网设置。
核心转换类
我的愿望是不包含任何非纯WPF的“第三方”……所以我必须找到一种方法将不支持的格式(如PDF)转换为我选择支持的格式。
基于后台的转换面板,我们有一个ContractParameter
类,它将所有选定选项分组。它被提供给BookFileConverter
类,该类通过BackgroundWorker
支持多线程(因为它在用户界面中使用报告进度)。然后转换过程通过基于输入和输出格式的两个接口进行:IReaderContract
和IWriterContract
。请注意,转换过程不管理多种输入/输出格式。
BookFileConverter
首先调用阅读器提取有用数据,然后通过写入器。该软件包包括ImageFileReader
、RARImageReader
、PDFImageReader
、XPSImageReader
用于读取文件夹、Rar/Zip、PDF或XPS文件中的图像(很快将扩展到ePUB)。我们可以通过ImageFileWriter、XPSImageWriter和ZIPWriter
类将文件写入图像文件、XPS或CBZ/ZIP文件。
从阅读器到写入器传输数据的标准方式是表示图像的字节数组,但我采取了一个捷径,例如RAR到图像的转换,其中阅读器直接从RAR中提取图像到文件夹,而不需要写入器。下面是用于转换的传输表模式
源 | Reader | 传输模式 | 写入器 | 目标 |
图像 | x | x | x | 图像 |
x | 直接 | 压缩文件夹 | CBZ/ZIP | |
提取到内存 | 内存 | 写入内存 | XPS | |
提取到内存 | 内存 | 写入文件 | 图像 | |
提取到内存 | 内存 | 写入文件 压缩文件夹 |
CBZ/ZIP | |
提取到内存 | 内存 | 写入内存 | XPS | |
CBR/RAR | 提取到文件夹 | 直接 | x | 图像 |
提取到文件夹 | 到临时文件夹 | 压缩文件夹 | CBZ/ZIP | |
提取到内存 | 内存 | 写入内存 | XPS | |
CBZ/ZIP | 提取到文件夹 | 直接 | x | 图像 |
x | x | x | CBZ/ZIP | |
提取到内存 | 内存 | 写入内存 | XPS | |
XPS | 提取到内存 | 内存 | 写入文件 | 图像 |
提取到内存 | 内存 | 写入文件 压缩文件夹 |
CBZ/ZIP | |
x | x | x | XPS | |
ePUB | x | x | x | 图像 |
x | x | x | CBZ/ZIP | |
提取到内存 | 内存 | 写入内存 | XPS |
MVVM 相关应用程序类
这是最复杂的部分……下面是参与该模式的应用程序中的类。我不会描述诸如ViewModelBase
、Mediator
或Messenger
之类的辅助类。它分为5个部分。视图:主用户界面、后台和模式之外的类。视图模型:主用户界面、后台和一些附加类。让我们逐一介绍它们。
视图层
主用户界面由一个MainView
(整个窗口)、一个ExplorerView
(左侧的库浏览器)和一些Ribbon内容(不同的书籍格式和设备视图)组成,这些内容由一个TabConbtrol
(不显示用户界面)托管,并绑定到ViewModel
集合。
InfoView
、OptionsView
、RecentFileView
和DeviceConfigView
是显示在Ribbon后台的面板。BookView
(用于漫画)、XpsBookView
(用于XPS文档)和ePUBBookView
(用于类似网络的查看器)托管在主窗口部分的TabControl中USBDeviceView
和随后的PhoneDeviceView
也托管在主窗口部分的TabControl中,但通过Ribbon组“设备”以上下文方式显示
ConvertView
和SimulateView
实际上脱离了该模式,因为BackgroundWorder
/线程或处于beta阶段的代码。
ViewModel 层
视图模型层也完全相同,此外还有一个BookViewModelBase
,它集中了所有常见的书籍功能;还有一个DeviceViewModelBase
,它对设备做同样的事情。请注意,它们都继承自ViewModelBase
(该类在其Data
属性中托管一个模型),而ViewModelBase
又继承自BindableObject
,后者实现了INotifyPropertyChanged
接口,用于绑定和MVVM支持。
右侧还有TreeviewItemViewModel
类,它派生出SysElementViewModel
(表示文件系统元素),并特化为SysDriveViewModel
、SysDirectoryViewModel
和SysFileViewModel
。它们用于USBDeviceViewModel
中,该ViewModel包含一个treeview
和一个listview
控件来显示设备内容。
更新...!
模型已通过FeedView/Model
等新类完成,以显示OPDS feeds……
主要变化是由于AvalonDock集成,它引入了从PaneViewModel
派生出的ToolViewModel
(表示文档和工具箱视图模型)。
交换:如何在层之间通信
我稍后会尝试发布一个关于交换的图表。方式是基于MVVM的:Views
调用ViewModel
上的Commands
,ViewModel
之间通过Mediator
通信,ViewModel
通过Messages
与Views
通信。我最大限度地避免了控件事件处理,但有时这是不可能或过于复杂的。
本地化引擎
这个新部分基于CodeProject的优秀文章《WPF Localization using resx》(所以我不再解释了……)。但是这个解决方案并不能让我满意,因为我不喜欢resx(当涉及到更新/复制/粘贴和重建新资源时,开发人员的工作量太大),而且我认为C.B.R.太小,不值得拥有这么多本地程序集……此外,还需要一个编辑器,以便任何用户都可以管理本地化或将资源存储打开到数据库中。
以下是支持XAML本地化的核心类。我只是对现有代码做了一些小改动,例如CultureManager
上的单例模式,或者重命名后的LocalizationExtension
,它现在有一个ResModul
属性,可以标识resx文件或xml/bin文件。为了完善由CultureManager
(单一公共类)表示的核心模型,我添加了一些管理功能,例如GetAvailableCultures
、GetAvailableModules
、GetModuleResource
、SaveResources
或CreateCulture
:这将满足编辑器的需求。请注意,最终,resx方法非常受限,无法覆盖所有这些重载……
我还添加了一个扩展CultureInfo
的CultureItem
类。它将映射到一个视图类,用于填充Ribbon中的语言库。
为了扩展resx模型,我选择向CultureManager
添加一个提供者模式。IResourceProvider
由ResxProvider
类(现有的不管理文化的类)和FileProviderBase
直接实现,后者派生出BinProvider
和XmlProvider
(我选择的解决方案,因为文件是人类可读的)。数据模型非常简单,将直接序列化或反序列化到资源文件。它由LocalizationFile
组成,该文件按文化代码对所有LocalizationDictionnary
进行分组,每个字典包含一个LocalizationItem
集合,表示一个资源集。有关其实现和用法的更多详细信息,请参阅编程摘录。
ePUB和OPDS模型
我对之前的模型进行了彻底的改造(之前的模型无法扩展到转换或写入功能)。ePubManager
是解析类,下面是模型类。这个模型要好得多,即使ePUB有很多规范版本……它也必须适合几乎所有的阅读。请注意,查看器基于IE(需要注册一些键以获得更好的仿真模式——由安装程序完成——您将在解决方案中找到它们)。如果检测到错误编码,可以使用视图上下文菜单。
OPDS 是一种专用于电子出版物的特殊订阅源格式。CBR 包含一个订阅源管理视图和一个浏览器视图,允许浏览 RSS 并下载书籍。下面是适合我需求(非 OPDS 规范)的模型,而 OpdsManager
是解析订阅源以创建视图和视图模型对象的主类。
代码:编程摘录
在下一章中,我将指出我面临的问题和解决方案,以及一些值得分享的优秀代码片段。
转换:提取PDF图像
我深入研究了许多解决方案——阅读和解析PDF、图像提取工具——然后偶然在网上找到了一份代码文件。通过一些测试文件,我发现iTextSharp有一个监听器,你可以将其插入解析器,以便在解析过程中获取专用事件和数据。实现一个带有IRenderListener
接口的类,RenderImage
方法将在每个图像上被调用,这样你就可以取回字节。
通过PdFReader
和PdfReaderContentParser
类处理PDF页面时,将其传递给ProcessContent
方法。请注意,您也可以获取文本事件。
try
{
reader = new PdfReader(inputFileorFolder);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
listener = new PDFImageListener();
for (int i = 1; i <= reader.NumberOfPages; i++)
{
parser.ProcessContent(i, listener);
}
...
更新...!
RenderImage
方法已通过此新代码得到改进。以前,/flat和/lzw未被处理,并且GetImageAsBytes
对于ITextSharp
可以读取的图像类型效果更好。
public void RenderImage(ImageRenderInfo renderInfo)
{
PdfImageObject image = renderInfo.GetImage();
PdfName filter = (PdfName)image.Get(PdfName.FILTER);
if (PdfName.DCTDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_JPG));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.JPXDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_JP2));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.LZWDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_TIF));
_ImageBytes.Add(image.GetImageAsBytes());
}
else if (PdfName.FLATEDECODE.Equals(filter))
{
_imageNames.Add(string.Format("{0:0000}_{1:0000}.{2}", PageIndex, _imageNames.Count, PdfImageObject.TYPE_PNG ));
_ImageBytes.Add(image.GetImageAsBytes());
}
...
新功能:按页合并图像(适用于PDF)
我过去的一些测试中注意到,PDF图像提取有时会导致数百张图像,因为每页都被切割成几部分。我不知道这是从哪里来的,但解决方案就在这里。我在转换面板中添加了一个复选框选项,以便在图像数量与页数不符时合并图像。
由于我的转换引擎基于一个阅读器(或提取器)、一个带有图像字节的管道,然后是一个写入器来完成所选的目标格式,因此我在中间放置了一个ImageJoiner
类,它将原始字节组合成真实的页面。
我首先修改ImageListener
以确保页面索引始终包含在图像文件名中。当我在PDFImageReader
类的Read
方法中检测到差异时,我将数组交给ImageJoiner
类,并将其数组作为结果。
if (settings.CheckResult && reader.NumberOfPages != listener.ImageNames.Count)
{
if (settings.JoinImages)
{
progress(string.Format("Extracting {0} : {1} images for {2} pages - Try to merge !", inputFileorFolder, listener.ImageNames.Count, reader.NumberOfPages));
ImageJoiner cp = new ImageJoiner();
cp.Merge(listener.ImageBytes, listener.ImageNames);
progress(string.Format("Merge to {0} new images...", cp.NewImageNames.Count));
imageBytes.AddRange(cp.NewImageBytes);
imageNames.AddRange(cp.NewImageNames);
ImageJoiner
相当简单,技巧在于第二个 Merge
方法:它将要处理的索引作为参数。我首先创建一个相应 BitmapImage
的列表,并计算目标位图的大小。然后创建一个匹配目标的 RenderTargetBitmap
。使用 DrawingVisual
,我打开它的上下文并在其中绘制所有位图。然后我要求 RenderTargetBitmap
绘制视觉效果,之后将字节提取到新数组中……就是这样!
public void MergeGroup(List<byte[]> imageBytes, List<string> imageNames, int start, int end, int index)
{
try
{
List<BitmapImage> bmps = new List<BitmapImage>();
double maxWidth = 0, maxHeight = 0, position = 0;
for (int i = start; i <= end; i++)
{
MemoryStream ms = new MemoryStream(imageBytes[i]);
BitmapImage myImage = new BitmapImage();
myImage.BeginInit();
myImage.StreamSource = ms;
myImage.CacheOption = BitmapCacheOption.None;
myImage.EndInit();
bmps.Add(myImage);
maxWidth = Math.Max(myImage.Width, maxWidth);
maxHeight += myImage.Height;
}
RenderTargetBitmap temp = new RenderTargetBitmap((int)maxWidth, (int)maxHeight, 96d, 96d, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using (DrawingContext ctx = dv.RenderOpen())
{
foreach (BitmapImage bi in bmps)
{
ctx.DrawImage(bi, new System.Windows.Rect(0, position, bi.Width, bi.Height));
position += bi.Height;
}
ctx.Close();
}
temp.Render(dv);
NewImageNames.Add(string.Format("{0:0000}.jpg", index));
NewImageBytes.Add(StreamToImage.BufferFromImage(temp));
bmps.Clear();
...
XPS:在固定页面上适配图像
如何在XPS文档页面上适配图像?这似乎是一个相当简单的问题,但在找到我的阿喀琉斯之踵之前,我不得不进行大量的测试……不要混淆像素图像大小和WPF单位!
请查看CBR.Core
中的XPSHelper
类。WriteDocument
方法包含将图像数组写入固定文档的所有机制。它调用WritePageContent
方法,用于写入XAML页面结构。在这里,我指定viewbox
以将图像适配到A4格式的页面。
private void WritePageContent(System.Xml.XmlWriter xmlWriter,
XpsResource res, double wpfWidth, double wpfHeight)
{
try
{
xmlWriter.WriteStartElement("FixedPage");
xmlWriter.WriteAttributeString("xmlns",
@"http://schemas.microsoft.com/xps/2005/06");
xmlWriter.WriteAttributeString("Width", "794");
xmlWriter.WriteAttributeString("Height", "1123");
xmlWriter.WriteAttributeString("xml:lang", "en-US");
xmlWriter.WriteStartElement("Canvas");
if (res is XpsImage)
{
xmlWriter.WriteStartElement("Path");
xmlWriter.WriteAttributeString("Data",
"M 0,0 L 794,0 794,1123 0,1123 z");
xmlWriter.WriteStartElement("Path.Fill");
xmlWriter.WriteStartElement("ImageBrush");
xmlWriter.WriteAttributeString("ImageSource",
res.Uri.ToString());
xmlWriter.WriteAttributeString
("Viewbox", string.Format("0,0,{0},{1}",
System.Convert.ToInt32(wpfWidth),
System.Convert.ToInt32(wpfHeight)));
这就是为什么我创建了一个WPF BitmapImage
来获取WPF单位的图像大小......!
//this is just to get the real WPF image size as WPF display units and
//not image pixel size !!
using (MemoryStream ms = new MemoryStream(images[i]))
{
BitmapImage myImage = new BitmapImage();
myImage.BeginInit();
myImage.CacheOption = BitmapCacheOption.OnLoad;
myImage.StreamSource = ms;
myImage.EndInit();
//write the page
WritePageContent(xmlWriter, xpsImage, myImage.Width, myImage.Height);
}
动态属性
CBR 没有数据库来存储图书信息,所以我必须找到一种方法来动态地向我的对象添加属性,以便它们可以扩展。假设您按系列管理所有图书……这不是我的Book
模型对象的现有属性。因此,您无法定义数据,也无法对它们进行分组或排序。另一个要求是它们需要是属性才能工作,并利用ObservableCollection
中图书的GroupDescriptions
和SortDescription
。请参阅下面“上下文菜单”一章。
.NET 4 带来了一个很棒但不太为人所知的功能:dynamic
或 Expando
对象。首先,根据它们为我们的模型添加一个新属性,如下所示。你添加给它的内容将被 WPF 和反射视为一个属性,但在代码中我们通常将其视为一个 Dictionary
。
private dynamic _dynamics = new ExpandoObject();
[Browsable(false)]
public dynamic Dynamics
{
get { return _dynamics; }
set { _dynamics = value; RaisePropertyChanged("Dynamics"); }
}
在定义了允许管理动态属性引用列表的后台面板之后,我能够使用它并通过同步方法刷新Book
模型。我遍历了两个字典来更新book
属性。
public void SynchronizeProperties(Book bk)
{
try
{
IDictionary<string, object> dict =
(IDictionary<string, object>)bk.Dynamics;
// add the properties from the settings if missing
foreach( string k in WorkspaceService.Instance.Settings.Dynamics )
{
if (!dict.Keys.Contains(k))
dict.Add(k, string.Empty);
}
// remove old properties that were removed from settings
foreach (string k in dict.Keys)
{
if (!WorkspaceService.Instance.Settings.Dynamics.Contains(k))
dict.Keys.Remove(k);
}
}...
填充后台文件信息面板很困难,因为WPF在绑定为PropertyName
/PropertyValue
列表时将其视为dictionary
。因此,我不得不将其转换为KeyValueProperty
视图模型类(派生自BindableObject
)的集合,该类具有PropertyChanged
事件,这样我就可以在动态属性中取回值。
我让您查看InfoViewModel
和KeyValueProperty
中的代码,以及ExplorerViewModel
和PropertyViewModel
中的代码,这些代码使用动态填充排序和分组下拉按钮菜单。
上下文菜单绑定
互联网上到处都指出,上下文菜单不共享相同的逻辑树视图……而且很容易获取父上下文……但在我的情况下,对于资源管理器中的上下文菜单,我还有额外的需求
- 使用
MainViewModel
中的命令,因为Read/Bookmark或Delete等命令已经实现 - 使用固定属性和
dynamic
属性填充排序和分组下拉菜单,所有这些都调用相同的命令
书籍上下文菜单
由于在逻辑树视图中向上获取父主窗口并绑定到这些命令并不容易,我选择在我的ExplorerViewModel
中实现一个中间命令,该命令将其转发给MainViewModel
。菜单项在XAML中定义如下
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Read" Command="{Binding ForwardCommand}"
CommandParameter="BookReadCommand">
然后,在ExplorerViewModel
中实现一个命令就很简单了,该命令使用Mediator
通过CommandContext
类通知其他视图发生了一个命令,该类包含作为string
的CommandName
和作为参数的Book
项。
private ICommand forwardCommand;
public ICommand ForwardCommand
{
get
{
if (forwardCommand == null)
forwardCommand = new DelegateCommand<string>(
delegate(string param)
{
Mediator.Instance.NotifyColleagues
(ViewModelMessages.ExplorerContextCommand,
new CommandContext()
{ CommandName = param,
CommandParameter =
this.Books.CurrentItem } );
},
delegate(string param)
{
if (CatalogData != null &&
Books.CurrentItem != null)
return true;
return false;
});
return forwardCommand;
}
}
在MainViewModel
上,我有一个方法,每当Mediator
调用注册的委托时,它都会通过反射调用原始命令。
internal void ExecuteDistantCommand(CommandContext context)
{
if (context != null)
{
new ReflectionHelper().ExecuteICommand
( this, context.CommandName, context.CommandParameter );
}
}
排序和分组菜单
我遇到了排序和分组菜单的相同问题,而且我必须找到一种方法来用Book
模型类属性填充它们,其中一些是动态属性。为了填充它,我在ExplorerViewModel
中定义了两个属性,如下所示,它们给我菜单项列表。我还使用反射从书中获取属性……但我很快发现我需要一个菜单项的视图模型,因为我需要的不仅仅是一个属性名称。
public List<PropertyViewModel> SortProperties
{
get { return GetSortProperties(); }
}
下面的PropertyViewModel
包含完全标识属性所需的数据,以及一个基于与上一章相同模型的命令,该命令将通知ExplorerViewModel
。
public class PropertyViewModel : ViewModelBase
{
...
#region ----------------PROPERTIES----------------
private object CatalogData { get; set; }
public string Prefix { get; set; }
public string ToDisplay { get; set; }
public string Name { get; set; }
...
#region generic command
private ICommand genericCommand;
public ICommand GenericCommand
{
get
{
if (genericCommand == null)
genericCommand = new DelegateCommand<string>
(ExecCommand,
...
void ExecCommand(string param)
{
if (param.Equals("group"))
Mediator.Instance.NotifyColleagues
(ViewModelMessages.ExplorerGroup, this.Name);
else
if (param.Equals("sort"))
Mediator.Instance.NotifyColleagues(
ViewModelMessages.ExplorerSort, this.Name);
}
...
}
请注意,在ExplorerViewModel
中,我们订阅了这两个消息,之后,我们可以修改集合上的SortDescription
和GroupDescription
。Group命令有点特殊,因为它需要将更改转发到View以在需要时删除GroupStyle
,这就是我调用Messenger
的原因。
Mediator.Instance.Register(
(Object o) =>
{
Group( o as string );
},
ViewModelMessages.ExplorerGroup);
...
internal void Group(string propertyName)
{
PropertyViewModel prop = GetGroupProperties().Find(p => p.Name == propertyName);
IEnumerable<PropertyGroupDescription> result =
Books.GroupDescriptions.Cast<PropertyGroupDescription>().
Where(p => p.PropertyName == prop.Prefix + prop.Name);
if (result != null && result.Count() == 1)
{
Books.GroupDescriptions.Remove(result.First());
}
else
{
Books.GroupDescriptions.Add(new PropertyGroupDescription
(prop.Prefix + prop.Name));
}
Messenger.Default.Send<MessageBase>( new MessageBase(this) );
RaisePropertyChanged("Books");
}
在XAML中,主要是要为菜单项定义正确的模板,包括显示和命令绑定。
<!-- style for menu items in sort/group dropdown buttons -->
<Style x:Key="PropertyViewModel">
<Setter Property="MenuItem.Header" Value="{Binding ToDisplay}"/>
<Setter Property="MenuItem.Command" Value="{Binding GenericCommand}" />
<Setter Property="MenuItem.IsCheckable" Value="true" />
</Style>
<Style x:Key="GroupMenuItemStyle" BasedOn="{StaticResource PropertyViewModel}" >
<Setter Property="MenuItem.CommandParameter" Value="group" />
</Style>
<Style x:Key="SortMenuItemStyle" BasedOn="{StaticResource PropertyViewModel}" >
<Setter Property="MenuItem.CommandParameter" Value="sort" />
</Style>
更新...!
基于相同的原理,我实现了一个基于Attribute
类的自动属性发现,允许我定义哪些属性可以排序、分组以及本地化代码……下面是示意图:PropertyHelper
类直接为我提供了填充菜单所需的PropertyViewModel
集合。
我将我的书籍模型扩展如下,以定义此属性不可分组,但可排序,并且本地化键是Core.Properties.FilePath。优点是如果我更改属性或语言,我的菜单现在会自动更新!
[UserPropertyAttribute(false, true, "Core.Properties.FilePath")]
public string FilePath
实现拖放
从外部应用程序
您可以将漫画文件或库拖放到应用程序以直接打开它。实现起来很简单,添加拖放事件处理程序,检查我们是否支持文件扩展名,并在MainViewModel
中执行打开命令。不要忘记通过AllowDrop
属性允许在您的主窗口上进行拖放。
private void RibbonWindow_Drop(object sender, DragEventArgs e)
{
try
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
MainViewModel mvm = DataContext as MainViewModel;
string[] droppedFilePaths =
e.Data.GetData(DataFormats.FileDrop, true) as string[];
if (FileService.Instance.FindCatalogFilterByExt
(System.IO.Path.GetExtension(droppedFilePaths[0])) != null)
mvm.CatalogOpenFileCommand.Execute
(droppedFilePaths[0]);
else
if (FileService.Instance.FindBookFilterByExt
(System.IO.Path.GetExtension(droppedFilePaths[0])) != null)
mvm.BookOpenFileCommand.Execute(droppedFilePaths[0]);
}
}
catch (Exception err)
{
ExceptionHelper.Manage("MainView:RibbonWindow_Drop", err);
}
}
资源管理器和设备视图
当拖放操作来自内部控件时,我们必须管理MouseDown
、MouseMove
事件,并在每个可能的源控件上启动拖放操作。为了简化操作,我开发了一个DragHelper
,您可以将其插入到每个需要作为源的控件上——这样您就不再关心鼠标处理和项目预览了。拖放目标需要根据您的需求照常管理。DragHelper
将其自身附加到构造函数中给定的控件。
public DragHelper( Control attachedView )
{
_Attached = attachedView;
_Attached.PreviewMouseLeftButtonDown +=
new MouseButtonEventHandler(PreviewMouseLeftButtonDown);
_Attached.PreviewMouseMove += new MouseEventHandler(PreviewMouseMove);
}
void PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_startPoint = e.GetPosition(null);
}
void PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && !IsDragging)
{
Point position = e.GetPosition(null);
if (Math.Abs(position.X - _startPoint.X) >
SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(position.Y - _startPoint.Y) >
SystemParameters.MinimumVerticalDragDistance)
{
Mouse.Capture(Application.Current.MainWindow);
StartDrag(e);
}
}
}
如果我们满足拖动距离,我们会在应用程序中捕获鼠标,并向附加的控件发送一个StartDragEvent
——这样它就可以准备数据并回调下面的DoDragDrop
。简而言之,它允许拖放,将自己附加到主窗口的DragOver
事件,创建一个包含源元素预览的装饰器,然后调用真实的DoDragDrop
函数。释放时,我们清理所有内容。
public void DoDragDrop( DataObject data, UIElement source )
{
// Let's define our DragScope .. In this case it is every thing inside
// our main window ..
DragScope = Application.Current.MainWindow.Content as FrameworkElement;
// We enable Drag & Drop in our scope ... We are not implementing Drop,
// so it is OK, but this allows us to get DragOver
bool previousDrop = DragScope.AllowDrop;
DragScope.AllowDrop = true;
// The DragOver event ...
DragEventHandler draghandler = new DragEventHandler(ScopeDragOver);
DragScope.PreviewDragOver += draghandler;
//Here we create our adorner..
_DragAdorner = new DragAdorner(DragScope, source, true, 0.5);
AdornerLayer layer = AdornerLayer.GetAdornerLayer(DragScope as Visual);
layer.Add(_DragAdorner);
DragDropEffects de = DragDrop.DoDragDrop(_Attached, data, DragDropEffects.Move);
// Clean up
Mouse.Capture(null);
DragScope.AllowDrop = previousDrop;
AdornerLayer.GetAdornerLayer(DragScope).Remove(_DragAdorner);
_DragAdorner = null;
DragScope.PreviewDragOver -= draghandler;
IsDragging = false;
}
如何使用它
使用它非常简单,只需在控件上实例化一个DragHelper
,如下所示:
_drager = new DragHelper(CatalogListBox);
_drager.OnStartDrag += new StartDragEventHandler(drag_OnStartDrag);
然后,创建StartDragEventHandler
,它将在拖放操作验证时由辅助程序调用。找到您的控件项并创建管理拖放所需的数据。调用DragHelper.DoDragDrop
函数以验证并继续操作。
void drag_OnStartDrag(object sender, MouseEventArgs e)
{
// Get the dragged ListViewItem
ListBoxItem item = VisualHelper.FindAnchestor<ListBoxItem>
((DependencyObject)e.OriginalSource);
if (item != null)
{
Book bk = (Book)CatalogListBox.ItemContainerGenerator.
ItemFromContainer(item);
// Find the data behind the item +
// Initialize the drag & drop operation
DataObject dragData = new DataObject("CBR.Book.Path", bk.FilePath);
_drager.DoDragDrop(dragData, item);
}
}
在任何目标控件中,根据需要处理_DragOver
、_DragLeave
和_Drop
处理程序。检查数据是否来自您,然后执行任何业务代码,如下所示,进行文件复制。
private void listViewContent_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent("CBR.Book.Path"))
{
string path = e.Data.GetData("CBR.Book.Path") as string;
string destFile = Path.Combine(
(this.FolderTree.SelectedItem as SysElementViewModel).FullPath,
Path.GetFileName(path) );
FileService.Instance.CopyToDevice(path, destFile,
this.cbDiskType.SelectedItem as DeviceInfo);
}
}
实现USB和设备检测
WMI事件监视器
WMI作为Windows管理工具是一个不错(但复杂)的功能,是我在一位朋友说“为什么不直接把我的书拖到我的阅读器里呢?”时发现的……哈哈,就这么简单吗?那么电子书阅读器支持的格式呢?这促使我开发了两个新功能:后台的DeviceConfigView
,它包含设备/制造商和支持的格式作为参考,以及新的USBDeviceView
,用于显示可以与设备关联的USB驱动器。
这个想法很简单,就是检测大多数电子书阅读器(以及后来的7款手机)的USB外设。经过数小时的互联网搜索和测试,无法直接实现,因为WMI提供了大量信息,但都没有相同的结构。这是我通过编写一个辅助类实现的解决方案,该类首先在连接时搜索现有设备,然后监视USB事件。
WMI提供了有用的类并支持一种名为WQL的查询语言:ObjectQuery
- 用于定义查询,ManagementObjectSearcher
- 类似于Execute
并返回ManagementBaseObject
的集合 - 包含所请求信息的结果。
首先,我需要两个ManagementEventWatcher
来获取针对Win32_USBControllerDevice
结构的WMI事件,用于__InstanceCreationEvent
和__InstanceDeletionEvent
。请注意,WITHIN
子句类似于计时器,并允许轮询事件。
addedWatcher = new ManagementEventWatcher
("SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance
ISA \"Win32_USBControllerDevice\"");
addedWatcher.EventArrived += new EventArrivedEventHandler(HandleAddedEvent);
addedWatcher.Start();
...
// Stop listening for events
if (addedWatcher != null)
addedWatcher.Stop();
处理创建事件有点复杂,因为插入设备时会创建很多东西。首先我们得到这个
Antecedent: \\FR-L25676\root\cimv2:Win32_USBController.DeviceID=
"PCI\\VEN_8086&DEV_293C&SUBSYS_00011179&REV_03\\3&21436425&0&D7"
Dependent: \\FR-L25676\root\cimv2:Win32_PnPEntity.DeviceID="USBSTOR\\
DISK&VEN_USB&PROD_FLASH_DISK&REV_1100\\AA04012700076941&0"
我从中提取了两条信息,允许我查询Win32_PnPEntity
- PNP设备ID,即
Win32_PnPEntity
键=> USBSTOR\\DISK&VEN_USB&PROD_FLASH_DISK&REV_1100\\AA04012700076941&0 - 设备 ID => AA04012700076941&0
结果是一个多行结果集。
我只在找到“USBSTOR
”时创建USB驱动器。然后我解析服务为“disk”的行以获取更多信息,并调用GetDiskInformation
,这与GetExistingDevices
相同。
ManagementBaseObject targetInstance =
e.NewEvent["TargetInstance"] as ManagementBaseObject;
DebugPrint(targetInstance);
// get the device name in the dependent property Dependent: extract the last part
string PNP_deviceID = Convert.ToString(targetInstance["Dependent"]).
Split('=').Last().Replace("\"", "").Replace("\\", "");
string device_name = Convert.ToString(targetInstance["Dependent"]).
Split('\\').Last().Replace("\"", "");
// query that device entity
ObjectQuery query = new ObjectQuery(string.Format("Select *
from Win32_PnPEntity Where DeviceID like \"%{0}%\"", device_name));
// check if match usb removable disk
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(query))
{
ManagementObjectCollection entities = searcher.Get();
//first loop to check USBSTOR
foreach (var entity in entities)
{
string service = Convert.ToString(entity["Service"]);
if (service == "USBSTOR")
device = new USBDiskInfo();
}
if (device != null)
{
foreach (var entity in entities)
{
string service = Convert.ToString(entity["Service"]);
if (service == "disk")
{
GetDiskInformation(device, device_name);
Devices.Add(device);
if (EventArrived != null)
EventArrived(this, new WMIEventArgs()
{ Disk = device, EventType = WMIActions.Added });
....
为了处理设备移除,我实现了这个方法:获取事件触发的实例,提取PNP设备ID,如果我们将设备从我们的集合中移除,则触发一个针对应用程序的内部事件。
private void HandleRemovedEvent(object sender, EventArrivedEventArgs e)
{
try
{
ManagementBaseObject targetInstance =
e.NewEvent["TargetInstance"] as ManagementBaseObject;
DebugPrint(targetInstance);
string PNP_deviceID = Convert.ToString(targetInstance
["Dependent"]).Split('=').Last().Replace
("\"", "").Replace("\\", "");
USBDiskInfo device = Devices.Find(x => x.PNPDeviceID == PNP_deviceID);
if( device != null )
{
Devices.Remove( device );
if (EventArrived != null)
EventArrived(this, new WMIEventArgs()
{ Disk = device, EventType = WMIActions.Removed });
}
}
....
对于现有设备,我从Win32_DiskDrive
经过Win32_DiskDriveToDiskPartition
再经过Win32_LogicalDiskToPartition
到达Win32_LogicalDisk
。这形成了一个查询和循环的级联,如下所示
ObjectQuery diskQuery = new ObjectQuery
("Select * from Win32_DiskDrive where InterfaceType='USB'");
foreach (ManagementObject drive in new ManagementObjectSearcher(diskQuery).Get())
{
ObjectQuery partQuery = new ObjectQuery(
String.Format("associators of {{Win32_DiskDrive.DeviceID='{0}'}}
where AssocClass = Win32_DiskDriveToDiskPartition", drive["DeviceID"])
);
DebugPrint(drive);
foreach (ManagementObject partition in new ManagementObjectSearcher
(partQuery).Get())
{
// associate partitions with logical disks (drive letter volumes)
ObjectQuery logicalQuery = new ObjectQuery(
String.Format("associators of {{Win32_DiskPartition.DeviceID='{0}'}}
where AssocClass = Win32_LogicalDiskToPartition", partition["DeviceID"])
);
DebugPrint(partition);
foreach (ManagementObject logical in new ManagementObjectSearcher
(logicalQuery).Get())
{
DebugPrint(logical);
USBDiskInfo disk = new USBDiskInfo();
ParseDiskDriveInfo(disk, drive);
ParseDiskLogicalInfo(disk, logical);
devices.Add(disk);
}
}
}
在应用程序中使用它
首先,在MainWindowView
中创建一个监听器实例,启动它并使用新的处理程序获取事件。不要忘记管理在应用程序启动前已连接的现有设备。
private WMIEventWatcher wmi = new WMIEventWatcher();
private void RibbonWindow_Loaded(object sender, RoutedEventArgs e)
{
wmi.StartWatchUSB();
wmi.EventArrived += new WMIEventArrived(wmi_EventArrived);
MainViewModel mvm = DataContext as MainViewModel;
if (mvm != null)
{
//add all existing disks
foreach (USBDiskInfo disk in wmi.Devices)
mvm.SysDeviceAddCommand.Execute(disk);
}
}
在事件处理程序中,由于它在另一个线程上被调用,我们使用应用程序调度程序来调用添加或移除设备的命令。这将更新/创建设备视图。不要忘记在主窗口的OnClose
事件中关闭监听器。
void wmi_EventArrived(object sender, WMIEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke
(DispatcherPriority.DataBind, (ThreadStart)delegate {
MainViewModel mvm = DataContext as MainViewModel;
if (mvm != null)
{
if (e.EventType == WMIActions.Added)
mvm.SysDeviceAddCommand.Execute(e.Disk);
else
if (e.EventType == WMIActions.Removed)
mvm.SysDeviceRemoveCommand.Execute(e.Disk);
}
});
}
本地化引擎
它是如何工作的?
为了完成核心模型的解释,所有事情都发生在LocalizationExtension
的GetValue
方法中。我扩展了这个现有方法,以请求配置的提供程序。ConvertValue
是相同的,GetObject
已在所有提供程序中被覆盖,以搜索请求的键。GetDefaultValue
中发生了更多更改,因为提供程序将创建文件、字典和资源项。
/// <summary>
/// Return the value associated with the key from the resource manager
/// </summary>
/// <returns>The value from the resources if possible otherwise the default value</returns>
protected override object GetValue()
{
if (string.IsNullOrEmpty(Key))
throw new ArgumentException("Key cannot be null");
object result = null;
IResourceProvider provider = null;
try
{
object resource = null;
//allow resource trapping by calling the handler
if (GetResource != null)
resource = GetResource(ResModul, Key, CultureManager.Instance.UICulture);
if (resource == null)
{
//get the provider
if (provider == null)
provider = CultureManager.Instance.Provider;
//get the localized resource
if (provider != null)
resource = provider.GetObject(this, CultureManager.Instance.UICulture);
}
//and then convert it to desired type
result = provider.ConvertValue(this, resource);
}
catch (Exception err)
{
}
try
{
// if it does not work, we ask the default value
if (result == null)
result = provider.GetDefaultValue(this, CultureManager.Instance.UICulture);
}
catch (Exception err)
{
}
return result;
}
在应用程序中使用它
通过设置LocalizeProvider
和LocalizeFolder
配置您想要的提供程序,然后用以下内容替换实际内容:ResModul
是目标文件,Key
是文件中的唯一资源标识符,DefaultValue
是显示的文本。无需添加额外的命名空间,因为资源扩展与标准XAML命名空间关联。
Text="{LocalizationExtension ResModul=CBR.Backstage, Key=ConvertView.Title, DefaultValue=Convert}"
请注意,只要您在使用它,本地化版本将在设计器和程序中创建并显示。这有助于快速识别您遗漏的内容。在运行时,所有运行文化的资源将自动添加到文件中,无需创建!
实现语言选择和编辑器
我设计了一个下拉按钮,它与一个图库相关联,该图库显示由CultureManager
的GetAvailableCulture
方法提供的CultureInfoItem
。它们映射到LanguageMenuItemViewModel
,该模型派生自MenuItemViewModel
,后者是MVVM模式中所有CBR菜单项的通用类。
下面相关的XAML定义了它
<Fluent:Gallery ItemsSource="{Binding Languages}"
GroupBy="Tag" x:Name="languageGallery" MinItemsInRow="1" MaxItemsInRow="2"
Orientation="Horizontal" SelectionChanged="languageGallery_SelectionChanged">
<Fluent:Gallery.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Icon}" Width="16" Height="16" />
<TextBlock Text="{Binding ToDisplay}" />
</StackPanel>
</DataTemplate>
</Fluent:Gallery.ItemTemplate>
<Fluent:Gallery.ItemContainerStyle>
<Style TargetType="{x:Type Fluent:GalleryItem}"
BasedOn="{StaticResource {x:Type Fluent:GalleryItem}}">
<Setter Property="Fluent:GalleryItem.IsSelected" Value="{Binding IsChecked}"/>
</Style>
</Fluent:Gallery.ItemContainerStyle>
</Fluent:Gallery>
编辑器是一个非常简单的工具窗口。在源窗格中,您选择要使用的语言和模块。单击“选择”按钮在应用程序中使用该语言。请注意,该窗口是无模式的,因此您可以继续使用C.B.R.来检查您的工作。网格显示选定的资源:Key、Default是开发时的原始文本,Translated是您需要处理并显示的内容。
在“新建”窗格中,您可以选择一个实际不存在的区域性,添加一个图标文件名,然后单击“创建”按钮在“内存”中添加一个新语言。使用“保存”按钮将您的更改提交到文件。
更新 1...!
这个臃肿的引擎并没有考虑从代码中进行本地化!在多文档支持的选项卡之前,我没有XAML之外的资源。但当涉及到消息或像“主页”和“设备”这样的选项卡标题时……MarkupExtension
会失败,因为它只适用于XAML!所以我找到了一个权宜之计。对于要在消息框中显示的消息,只需请求资源即可。选项卡需要在语言更改时通过ViewModel更新。
我在CultureManager
中编写了一个新函数,用于基于相同原理请求本地化……GetLocalization
(string modul, string key, string defaultValue)将通过提供程序查找是否存在现有LocalizationExtension
或LocalizationItem
。如果不存在,它将像原始引擎一样管理所有内容。
要将其放入ViewModel
中,例如我的“Home”选项卡,我必须在文化更改时订阅CultureManager
事件……并在Dispose
方法中取消订阅。就是这样……花费了数小时才找到一个优雅的解决方案!
public HomeViewModel()
{
CultureManager.Instance.UICultureChanged += new CultureEventArrived(Instance_UICultureChanged);
DisplayName = CultureManager.Instance.GetLocalization("ByCode", "DocumentTitle.Home", "Home");
}
void Instance_UICultureChanged(object sender, CultureEventArgs e)
{
DisplayName = CultureManager.Instance.GetLocalization("ByCode", "DocumentTitle.Home", "Home");
}
protected override void OnDispose()
{
base.OnDispose();
CultureManager.Instance.UICultureChanged -= new CultureEventArrived(Instance_UICultureChanged);
}
更新 2...!
现在,文化信息通过 .NET 类 CultureInfo
中的 IETF 代码进行标识。它结合了语言和国家代码,例如 fr-FR:法国法语。fr-CA 是加拿大法语。CultureItem
类已被移除,图标现在也根据 IETF 代码命名。
更新 3...!
本地化编辑器已得到改进,现在显示一个关于未使用资源的额外列……然后它已通过WPF拼写检查完成。请注意,您将需要安装.net语言包!顺便说一句,流畅的图库现在可以正确显示,底部带有管理按钮。
扩展引擎
如果您想用数据库提供程序等来扩展它,请编写您的模型和提供程序,并随时向我们提供反馈!我另一个项目需要它 " src="https://codeproject.org.cn/script/Forums/Images/smiley_smile.gif" />
请注意,CBR并未完全本地化测试版项目和工具提示...
3D和2页翻页阅读器
长期以来,我一直在关注Mistu Futura的“两页书”,但我不太确定绑定到50多张位图页面的ItemControl的性能……最近我经过短暂测试发现,绑定只发生在翻页时!所以我决定进一步研究,并根据我的需求调整这个控件。原始代码文档不完善,内容基于“全屏适配”和xaml用户控件,因此没有缩放问题……所以我必须找到一种解决方案,让位图始终尽可能最好地适配页面。我还要求它在滚动视图中,并响应我当前的图书命令。
它是如何工作的?
首先,将所有原始代码放入一个文件夹中,更改类名以使其更清晰、分组,并将用户控件转换为一个控件。这使得我们得到了LinearGradiantHelper
、PageParameters
、CornerOrigin
和PageStatus
(无更改)——以及TwoPageBook
(新的主控件)和TripleSheet
控件,后者代表了书本右侧或左侧的3个页面。当动画一个页面时,我们可以看到它在我们面前的一侧,后面的一侧(当它翻页时)以及前一页(或后一页)的正面。
我修改了控件以使其具有默认样式,并将其包装在一个滚动视图中,如下图所示。我删除了一些我不需要的东西,例如当前页面的缩放功能……
<Style x:Key="{x:Type local:TwoPageBook}" TargetType="{x:Type local:TwoPageBook}">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TwoPageBook}">
<ScrollViewer Name="PART_ScrollViewer" Focusable="True"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid Name="PART_Content">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="50*" />
</Grid.ColumnDefinitions>
<local:TripleSheet Grid.Column="0" x:Name="sheet0" HorizontalAlignment="Right"
IsTopRightCornerEnabled="false" IsBottomRightCornerEnabled="false" />
<local:TripleSheet Grid.Column="1" x:Name="sheet1" HorizontalAlignment="Left"
IsTopLeftCornerEnabled="false" IsBottomLeftCornerEnabled="false" />
</Grid>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
然后,我完善了OnApplyTemplate
方法,以确保_Content
将缩放,并且_ScrollContainer
将稍后用于适应文档视图。我还添加了Scale
、FitMode
和CurrentPageIndex
,因为原始代码管理一个页码索引(索引除以2)。然后我更改了TripleSheet
模板,使其内容适合,并用颜色填充位图无法正确缩小时的空白。请注意,Fit
方法通过使用Page
模型类,打破了控件的通用性……这是找到内容尺寸的唯一方法!—— src="https://codeproject.org.cn/script/Forums/Images/smiley_frown.gif" /> /p>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_ScrollContainer = (ScrollViewer)GetTemplateChild("PART_ScrollViewer");
_Content = (FrameworkElement)GetTemplateChild("PART_Content");
_scaleTransform.CenterX = 0.5;
_scaleTransform.CenterY = 0.5;
_Content.LayoutTransform = _scaleTransform;
[...]
private void Fit()
{
if (FitMode == DisplayFitMode.Height)
{
Scale = (this._ScrollContainer.ViewportHeight - FIT_BORDER) / (Items[CurrentSheetIndex] as CBR.Core.Models.Page).Image.Height;
}
else if (FitMode == DisplayFitMode.Width)
{
Scale = (this._ScrollContainer.ViewportWidth - FIT_BORDER) / ((Items[CurrentSheetIndex] as CBR.Core.Models.Page).Image.Width * 2);
}
}
出于性能考虑,我将我的书籍视图拆分为两个独立的类:单页视图和双页视图。天哪!我必须管理两种模式之间的切换和参数传递!由于书籍已加载,我只需要当前页码索引。书籍视图通过主视图进行通信,主视图接收到SwapTwoPageView
消息,并将视图转换为所需的视图,如下图所示。
Mediator.Instance.RegisterHandler<BookViewModelBase>(ViewModelMessages.SwapTwoPageView,
(BookViewModelBase o) =>
{
SwapTwoPageMode(o);
});
[...]
internal void SwapTwoPageMode(BookViewModelBase o)
{
ViewModels.Remove(o);
BookViewModelBase newModel = null;
BookViewModelBase oldModel = null;
if (o is ComicViewModel)
{
ComicViewModel comic = o as ComicViewModel;
newModel = new TwoPageViewModel(o.Data, comic.CurrentPage.Index, comic.FitMode, comic.PreviousScale);
}
else
{
TwoPageViewModel comic = o as TwoPageViewModel;
newModel = new ComicViewModel(o.Data, comic.CurrentPageIndex, comic.FitMode, comic.PreviousScale);
}
oldModel = o;
ViewModels.Add(newModel);
SetActiveView(newModel);
ViewModels.Remove(oldModel);
}
请注意,我发现的许多书籍并不真正符合此视图,因为页面比例从未相同或包含双重扫描。请不要惊讶,但由于我不处理图像并将其加载到内存中,所以我无法进行任何调整。
贡献者
非常感谢SevenZip(它允许我在内存中解压缩)、Fluent Ribbon这个出色的控件库,以及所有在这个项目中我阅读过的互联网贡献者。
您的反馈
由于上一个版本发布时间相当长,0.6版本下载量约为6万次,我想您可能对提供更精确的反馈感兴趣,我将所有计划中的功能都放在了项目的“问题跟踪器”页面。现在您可以投票或创建新项目,就您的需求优先级向我提供反馈。这将帮助我规划下一个版本的内容。
交付物 - 安装注意事项
使用0.7版和安装程序时,请务必删除以前的版本,否则有些文件将不会被替换,从而导致大问题。我将努力寻找更好的解决方案。
CodeProject上不再提供下载,因为有4个大尺寸的交付物:安装程序9MB,源代码16MB,直接二进制文件7MB。请访问此链接获取所需文件。
结论
我希望您能像我一样喜欢它,并能在我的代码或软件中找到您所需。如果您对我遇到的问题有更好的解决方案,请不吝赐教……
历史记录(简化版...)
- v0.7
- Ribbon 选项卡和命令已重新组织
- ePUB:彻底重构
- 为 OPDS 流添加了一个新的专用订阅源查看器
- 本地化引擎改进:添加了按代码管理资源的功能,现在基于ietf语言/国家代码,从xml中移除图像(基于代码),移除CultureItem类。更好的复制、管理视图和图库,拼写检查
- 动态图书:框架设计器现在采用 MVVM 模式,并且效果更好。它基于 ItemControl,并使用 Canvas 进行模板化。还添加了 7Phone 和平板模拟器。
- 发布模式现在为“Any CPU”,以与运行平台兼容。添加了32位和64位版本的7z.dll。x32和x64安装程序,用于epub/html视图的ie仿真模式注册表项。
- 多文档:C.B.R.现在提供多文档显示选项,带有标签页 - 集成定制的AvalonDock 2库。
- 改进了PDF图像提取功能,并新增了图像合并转换选项
- 新的 BrowseForControl
- 定制的XPS查看器,用于去除工具栏并将其绑定到CBR命令
- 在主页添加快速入门手册和按钮,管理内部CBR订阅源语言