在开发Windows应用程序时,经常需要一种方法来预览文件内容。这可能是为了调试、错误报告或者简单地为了查看文件数据。本文将介绍一种轻量级的文件预览控件,该控件可以在WTL应用程序中使用,支持以文本、十六进制或图像格式预览文件。
在开发一个名为CrashRpt的Windows应用程序错误报告库时,需要一种方法来预览文件内容。CrashRpt库在应用程序崩溃时会生成一个包含错误报告的归档文件,其中可能包含崩溃转储、错误日志、桌面截图等。用户在将错误报告发送到互联网之前需要能够查看这些文件的内容。因此,需要一个控件来预览文件内容,包括十六进制、文本和图像格式。
将控件集成到WTL应用程序中非常简单。只需要将FilePreviewCtrl.h和FilePreviewCtrl.cpp文件复制到项目目录中,并将这些文件添加到Visual C++项目中。在对话框上放置一个静态控件,并将静态控件的名称设置为IDC_PREVIEW。接下来,在对话框的头文件开头添加#include "FilePreviewCtrl.h"行,并在对话框的(或窗口的)类中添加CFilePreviewCtrl m_filePreview成员变量。最后,在OnInitDialog()处理程序中,通过添加以下代码行来子类化静态控件:
m_filePreview.SubclassWindow(GetDlgItem(IDC_PREVIEW));
CFilePreviewCtrl类提供了几种方法,可以用来预览文件并自定义控件的行为。要打开文件进行预览,请使用SetFile()方法。要获取当前预览的文件名,请使用GetFile()方法。
// 返回当前文件的文件名
LPCTSTR GetFile();
// 设置当前文件并预览模式。
// 可以将文件名设置为NULL以清除预览。
BOOL SetFile(LPCTSTR szFileName, PreviewMode mode=PREVIEW_AUTO);
要设置当前预览模式,请使用SetPreviewMode()方法。使用GetPreviewMode()可以获取当前预览模式。
// 返回当前预览模式
PreviewMode GetPreviewMode();
// 设置当前预览模式
void SetPreviewMode(PreviewMode mode);
预览模式由PreviewMode枚举定义。如所见,文件预览控件可以自动检测预览模式(PREVIEW_AUTO常量),或者可以通过指定PREVIEW_HEX、PREVIEW_TEXT或PREVIEW_IMAGE常量来强制使用另一种预览模式。
// 预览模式
enum PreviewMode
{
PREVIEW_AUTO = -1, // 自动
PREVIEW_HEX = 0, // 十六进制
PREVIEW_TEXT = 1, // 文本
PREVIEW_IMAGE = 2 // 图像
};
可以使用DetectPreviewMode()方法来确定对于某个文件自动选择的预览模式。
// 为某个文件确定正确的预览模式
PreviewMode DetectPreviewMode(LPCTSTR szFileName);
当没有内容可以预览时,文件预览控件会显示一个空白屏幕,并在顶部显示"No data to display"消息。可以通过使用SetEmptyMessage()方法来覆盖文本消息。
// 设置当没有内容预览时显示的文本(默认是"No data to display")
void SetEmptyMessage(CString sText);
对于十六进制预览模式,可以通过调用SetBytesPerLine()方法来修改每行显示的字节数。
// 设置十六进制预览中每行的字节数
BOOL SetBytesPerLine(int nBytesPerLine);
文件预览控件如何自动检测正确的预览模式?它通过两种方式进行:文件扩展名和文件头部字节。首先,它检查文件扩展名。如果文件扩展名是TXT、INI、LOG、XML、HTM、HTML、JS、C、H、CPP、HPP,则控件假定这是一个文本文件。如果不是,控件加载文件的前几个字节,并将其与BMP文件签名进行比较(所有BMP文件在文件开头都有"BM"魔术字符)。如果签名匹配,控件假定该文件是位图图像文件。如果不是,控件假定该文件是一个未知的二进制文件,并为其选择十六进制预览模式。
关于这个控件如何如此快速地预览大型文本、图像和二进制文件。两件事有助于其预览速度:使用文件映射和多线程。
文件映射是一个Win32对象,允许将任意大的文件映射到操作系统内存中,并通过创建文件视图来访问文件的任何部分。这样,可以快速访问大型二进制文件的任何部分,而不会浪费内存,也不会有时间延迟。可以在FilePreviewCtrl.h头文件中找到CFileMemoryMapping类。下面展示了CFileMemoryMapping类的声明:
// 用于将文件内容映射到内存
class CFileMemoryMapping
{
public:
CFileMemoryMapping();
~CFileMemoryMapping();
// 初始化文件映射
BOOL Init(LPCTSTR szFileName);
// 关闭文件映射
BOOL Destroy();
// 返回内存映射文件的大小
ULONG64 GetSize();
// 为内存映射文件的一部分创建视图
LPBYTE CreateView(DWORD dwOffset, DWORD dwLength);
private:
HANDLE m_hFile; // 当前文件的句柄
HANDLE m_hFileMapping; // 内存映射对象
DWORD m_dwAllocGranularity; // 系统分配粒度
ULONG64 m_uFileLength; // 文件的大小
CCritSec m_csLock; // Sunc对象
std::map m_aViewStartPtrs; // 文件视图的基址
};
CFileMemoryMapping::Init()方法使用CreateFile()和CreateFileMapping()WinAPI函数来初始化文件映射对象。创建文件映射实际上并不分配内存。内存分配是在CFileMemoryMapping::CreateView()方法中执行的,该方法使用MapViewOfFile()API调用来内存映射文件的一小部分(视图),并返回指向它的指针。当不再需要分配的视图时,它通过UnmapViewOfFile()API调用来取消映射。
CFileMemoryMapping类允许同时创建多个视图,以便从不同的线程同时访问它们。创建的视图存储在CFileMemoryMapping::m_aViewStartPtrs变量中。
当需要执行耗时的工作而不阻塞主线程时,就会使用多线程。为了异步执行工作,使用CreateThread()WinAPI函数创建另一个线程,并称之为工作线程。文件预览控件在另一个线程中执行文本文件解析(需要解析文本文件以确定行分隔符)。它也在另一个线程中加载图像。可以通过查看CFilePreviewCtrl::DoInWorkerThread()私有方法的代码来了解它是如何做到的。
当工作线程执行图像加载或文本解析时,主线程在定时器事件(WM_TIMER消息)上显示已准备好预览的部分。滚动条也在定时器上更新。当工作线程完成文件加载时,它发送WM_FPC_COMPLETE私有消息到文件预览控件窗口,以通知其完成。