ImageFanReloaded






4.76/5 (7投票s)
.NET 4.5.1 轻量级图像查看器,支持多核处理
引言
ImageFanReloaded 是一款轻量级的图像查看器,支持 .NET 4.5.1 并利用多核处理能力。
ImageFanReloaded 是一项全新的尝试,它在原有 ImageFan 项目的功能基础上进行了复制和改进,同时采用了现代化的技术(WPF、.NET Framework 4.5.1、Moq)以及强大的设计原则和实践(抽象工厂、依赖倒置、模型-视图-演示者、异步编程的 async/await、交互式测试)。
背景
我一直希望能够利用 .NET 的强大功能来创建一个图像查看器,为现有的 C/C++ 解决方案提供一个托管式的替代品。此外,缺乏 64 位图像查看器也促使我寻求一个 .NET 解决方案,在硬件和软件平台允许的情况下,该解决方案能够隐式地以 64 位应用程序的形式运行。
视觉布局和功能
该应用程序的设计遵循现代图像查看器的传统风格,如下所示:
- 窗口左侧是文件夹和驱动器树
- 窗口右侧是缩略图面板,显示选定树节点文件夹或驱动器中图像的缩略图
选择特定文件夹或驱动器会用其中包含的缩略图填充缩略图面板。可以通过 WASD、箭头键、空格键-退格键以及 PgUp-PgDn 键来导航缩略图列表。可以通过使用配置的按键导航到缩略图并单击它,或者通过全屏视图或图像视图导航来选中缩略图。
如果缩略图被选中时被单击,或者按下 Enter 键,它将设置图像为全屏视图,并在必要时调整图像大小以适应屏幕。在全屏模式下,可以使用 WASD、箭头键、空格键-退格键以及 PgUp-PgDn 键和鼠标滚轮来遍历图像列表。用户可以通过按 Esc 键退出此模式。
在全屏模式下,单击或按 Enter 键会切换到窗口化显示模式,以完整尺寸显示图像,如果图像的任一维度大于相应屏幕尺寸维度,则可滚动。在窗口化模式下,可以使用 WASD、箭头键、空格键-退格键以及 PgUp-PgDn 键来遍历图像列表。用户可以通过单击或按 Esc 或 Enter 键退出此模式。
实现挑战与限制
项目结构如图所示。我将逐一解释每个相关的源代码元素。
MainPresenter、IMainView 和 MainView 类型
这三个类型以及支持的模型类型,采用了 Model-View-Presenter (MVP) 模式,用于将 UI 与业务逻辑代码解耦。虽然 WPF 通常与 Model-View-ViewModel (MVVM) 模式结合使用,但在图像可视化应用程序的情况下,我更倾向于 MVP 模式,因为它更灵活,同时保持了依赖倒置和关注点分离的优雅。
在以前的 ImageFan 项目中,有一个名为 TasksDispatcher
的类,它将单个缩略图生成任务分配到可用的处理器核心数量上。在这个项目中,我依赖于 .NET 4.5.1 中 exposed 的细粒度并发支持(通过 async-await 模式)。因此,任务分派器的职责已经由 MainPresenter
类型中的一个异步方法承担,大大减少了代码量并简化了逻辑。
private async void PopulateThumbnails(string folderPath)
{
var entered = false;
try
{
Monitor.Enter(_populateThumbnailsLockObject, ref entered);
using (_cancellationTokenSource = new CancellationTokenSource())
{
var token = _cancellationTokenSource.Token;
var getImageFilesTask = Task.Run<IEnumerable<ThumbnailInfo>>(() =>
{
var imageFiles = TypesFactoryResolver.TypesFactoryInstance
.DiscQueryEngineInstance
.GetImageFiles(folderPath);
var thumbnailInfoCollection =
imageFiles
.Select(anImageFile => new ThumbnailInfo(anImageFile))
.ToArray();
return thumbnailInfoCollection;
});
var thumbnails = await getImageFilesTask;
if (token.IsCancellationRequested)
return;
_mainView.ClearThumbnailBoxes();
for (var thumbnailCollection = thumbnails;
thumbnailCollection.Any();
thumbnailCollection =
thumbnailCollection.Skip(GlobalData.ProcessorCount))
{
var currentThumbnails =
thumbnailCollection.Take(GlobalData.ProcessorCount);
var readThumbnailInputTask = Task.Run<TaskStatus>(() =>
{
foreach (var aThumbnail in currentThumbnails)
{
if (token.IsCancellationRequested)
return TaskStatus.Canceled;
aThumbnail.ImageFile.ReadThumbnailInputFromDisc();
}
if (token.IsCancellationRequested)
return TaskStatus.Canceled;
else
return TaskStatus.RanToCompletion;
}, _cancellationTokenSource.Token);
var readThumbnailInputTaskStatus = await readThumbnailInputTask;
if (readThumbnailInputTaskStatus == TaskStatus.Canceled)
return;
_mainView.PopulateThumbnailBoxes(currentThumbnails);
var getThumbnailsTask = Task.Run(() =>
{
currentThumbnails
.AsParallel()
.AsOrdered()
.ForAll(aThumbnail =>
{
if (!token.IsCancellationRequested)
{
var currentThumbnail = aThumbnail.ImageFile.Thumbnail;
currentThumbnail.Freeze();
aThumbnail.ThumbnailImage = currentThumbnail;
}
});
}, _cancellationTokenSource.Token);
}
}
}
finally
{
if (entered)
Monitor.Exit(_populateThumbnailsLockObject);
}
}
FileSystemEntryTreeViewItem 类型
此类型是一个 WPF 用户控件,扩展了 TreeViewItem
控件。每个这样的控件都在 WPF TreeView
控件中显示驱动器或文件夹名称及其图标。当选中一个 FileSystemEntryTreeViewItem
节点时,它会触发缩略图面板中缩略图的显示。
ThumbnailBox 类型
此类型是一个 WPF 自定义控件,继承自 UserControl
类型。它是一个可变大小的控件,显示一个图像缩略图框,其中包含图像缩略图本身,并装饰有图像文件名。它的最大尺寸(宽度或高度)将缩放到 GlobalData.ThumbnailSize
(200 像素),而另一个尺寸则按比例缩放。
ImageView 类型
ImageView
是一个 WPF 窗口,以对话框形式显示,具有全屏和窗口化两种模式。全屏模式会调整图像大小(如果图像大于可用全屏尺寸),同时保持原始纵横比。窗口化模式则保留图像大小,在图像超出屏幕尺寸时允许滚动。
DiscQueryEngine 类型
此类型返回系统光盘驱动器和特殊文件夹的列表,以及指定驱动器或文件夹内的子文件夹和图像文件。
ImageFile 类型
该类型仅在需要时从磁盘读取图像文件。它返回从磁盘读取的原始图像、图像的缩略图或图像的全屏缩放版本。它在内部使用可 Dispose 的 Image
对象,然后将结果转换为 WPF 特有的不可 Dispose 的 ImageSource
对象。ImageFile
仅表现出部分 IDisposable
行为,因此它不实现 IDisposable
接口。
ImageResizer 类型
此类型包含两个图像缩放操作:将图像缩放到具有指定最大尺寸的缩略图,以及将图像缩放到给定宽度和高度。
GlobalData 类型
此类包含应用程序范围内的常量和只读数据的引用:资源图像、缩略图大小、导航键和系统处理器核心数。
测试
在开发视觉密集型应用程序时,自然会提出这样的问题:“什么可以被自动测试?”。正确的答案是:“相当多。” ImageFanReloaded
在设计时就考虑了可测试性。使用的测试框架是 MSTest
。
该应用程序包含经典的单元测试、交互式单元测试(使用 Moq
)和功能测试。所有逻辑都依赖于抽象接口,而不是具体类型,因此支持依赖倒置原则。为了便于测试,使用了 Abstract Factory
设计模式,一个工厂用于正常的生产环境,另一个工厂则返回存根实例用于测试。
总结
从头开始构建应用程序,同时保持其 Windows Forms
.NET 2.0 前身 ImageFan 项目的要求,这是一项有益的尝试。
它使我能够迁移到更新的技术(WPF
),打破原有的类型之间的依赖关系,使用 Model-View-Presenter
模式,添加测试,采用更简单的缩略图生成并发模型等等。
就使用应用程序的感知满意度而言,我发现这个 WPF
实现更流畅、更精巧,响应速度更快,缩略图刷新也更令人愉悦,当滚动缩略图面板时,几乎感觉不到刷新。缺点是内存占用略大,同时 WPF
依赖于不可 Dispose 的 ImageSource
类型(而非 Windows Forms
的可 Dispose 的 Image
类型),这导致在预期的垃圾回收发生之前,内存使用会出现一些峰值。
由于它是由托管语言 (C#.NET) 编写的,ImageFanReloaded
是即时编译的,专门针对用户的平台(x86 或 x64)。因此,它使用单个二进制文件,同时提供运行平台的全部优势。
源代码和应用程序下载
ImageFanReloaded
应用程序的完整源代码(一个 Google Code 项目)可以通过 此处 访问。如果只对二进制文件感兴趣,可以从 此链接 下载。
我非常欢迎为这个 ImageFan 开源 (GPL v3) 项目 做出贡献和提供反馈。
参考文献
- [1] Microsoft Developer Network (MSDN) 页面
- [2] Robert C. Martin, Clean Code, Prentice Hall, 2008
- [3] Roy Osherove, The Art of Unit Testing: with Examples in .NET, Manning Publications, 2009
历史
- 版本 0.1 - 首次提交 - 2015 年 3 月 2 日
- 版本 0.2 - 更新源代码和二进制文件,少量文章编辑 - 2015 年 3 月 13 日