在 WPF 应用程序中使用 Vista 预览处理程序






4.72/5 (14投票s)
本文介绍如何在 WPF 应用程序中使用 Windows Vista 预览处理程序
引言
首先,什么是预览处理程序?预览处理程序是一个 COM 对象,在您想要显示项目预览时会被调用。换句话说,预览处理程序是文件内容的一种轻量级、丰富且只读的预览,显示在阅读窗格中。您可以在 Microsoft Outlook 2007、Windows Vista 甚至有时在 XP 中找到预览处理程序。我们可以在 WPF 应用程序中使用预览处理程序吗?也许可以。让我们看看如何做到这一点。

让我们开始吧
让我们创建一个简单的 WPF 窗口,它在左侧显示文件列表,在右侧显示项目预览。我们将使用一个简单的文件列表string
集合作为我们的数据源,将其绑定到Listbox
的 Items,然后将选定的项绑定到某个contentpresenter
。我之前已经写过关于这种方法的博客。
<Grid DataContext={StaticResource files}>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".2*"/>
<ColumnDefinition Width=".8*"/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource={Binding} IsSynchronizedWithCurrentItem="True" />
<ContentPresenter Grid.Column=”1” Content={Binding Path=/}/>
<GridSplitter Width="5"/>
</Grid>
我们的数据源应该随着文件系统的变化而自动更新。因此,这是一个使用FileSystemWatcher
对象的好机会。
class ListManager:ThreadSafeObservableCollection<string>
{
string dir =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public ListManager()
{
FileSystemWatcher fsw = new
FileSystemWatcher(dir);
fsw.NotifyFilter =
NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
fsw.Created += new FileSystemEventHandler(fsw_Created);
fsw.Deleted += new FileSystemEventHandler(fsw_Deleted);
fsw.EnableRaisingEvents = true;
string[] files = Directory.GetFiles(dir);
for (int i = 0; i < files.Length; i++)
{
base.Add(files[i]);
}
}
void fsw_Deleted(object sender, FileSystemEventArgs e)
{
base.Remove(e.FullPath);
}
void fsw_Created(object sender, FileSystemEventArgs e)
{
base.Add(e.FullPath);
}
}
现在,在应用了简单的DataTemplate
之后,我们可以在应用程序的左侧窗格中看到文件列表。当某个目录下的文件发生更改时,它将自动更新。
下一步是了解如何在自定义应用程序中使用预览处理程序。毕竟,预览处理程序是一个实现了以下接口的常规 COM 对象
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("8895b1c6-b41f-4c1c-a562-0d564250836f")]
interface IPreviewHandler
{
void SetWindow(IntPtr hwnd, ref RECT rect);
void
SetRect(ref RECT rect);
void DoPreview();
void Unload();
void SetFocus();
void QueryFocus(out IntPtr phwnd);
[PreserveSig]
uint TranslateAccelerator(ref MSG pmsg);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b7d14566-0509-4cce-a71f-0a554233bd9b")]
interface
IInitializeWithFile
{
void
Initialize([MarshalAs(UnmanagedType.LPWStr)] string pszFilePath, uint grfMode);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
interface
IInitializeWithStream
{
void Initialize(IStream pstream, uint
grfMode);
}
为了查找并为特定文件类型附加预览处理程序,我们只需要查看HKEY_CLASSES_ROOT
并找到预览处理程序的 COM Guid(8895b1c6-b41f-4c1c-a562-0d564250836f
)。此键的默认值将是实际可以预览此文件类型的 COM 对象的 Guid。让我们这样做
string CLSID = "8895b1c6-b41f-4c1c-a562-0d564250836f";
Guid g = new Guid(CLSID);
string[] exts = fileName.Split('.');
string ext = exts[exts.Length - 1];
using (RegistryKey hk = Registry.ClassesRoot.OpenSubKey
(string.Format(@".{0}\ShellEx\{1:B}", ext, g)))
{
if (hk != null)
{
g = new Guid(hk.GetValue("").ToString());
现在,我们知道该文件可以被预览。因此,让我们初始化适当的 COM 实例作为预览处理程序
Type a = Type.GetTypeFromCLSID(g, true);
object o = Activator.CreateInstance(a);
预览处理程序有两种初始化方式——基于文件和基于流。每种方式都有自己的接口。因此,我们只能检查创建的对象是否实现了该接口,才能初始化处理程序。
IInitializeWithFile fileInit = o as IInitializeWithFile;
IInitializeWithStream streamInit = o as IInitializeWithStream;
bool isInitialized = false;
if (fileInit != null)
{
fileInit.Initialize(fileName, 0);
isInitialized = true;
}
else
if (streamInit != null)
{
COMStream stream = new
COMStream(File.Open(fileName, FileMode.Open));
streamInit.Initialize((IStream)streamInit, 0);
isInitialized = true;
}
在初始化了处理程序之后,我们可以设置一个句柄,指示处理程序应该放置在哪个窗口中。我们还应该向处理程序提供窗口区域的边界,以便其放置在该区域中。
if (isInitialized)
{
pHandler = o as IPreviewHandler;
if (pHandler != null)
{
RECT r = new
RECT(viewRect);
pHandler.SetWindow(handler, ref r);
pHandler.SetRect(ref r);
pHandler.DoPreview();
}
}
到目前为止一切顺利,但我们是在 WPF 中。因此,我们使用的ContentPresenter
没有句柄!这是正确的,但是主 WPF 应用程序窗口有。所以,让我们先获取主应用程序窗口的句柄,然后创建ContentControl
所占据区域的矩形边界。
为了做到这一点,我们将从ContentPresenter
派生,并监听其ActualHeight
和ActualWidth
属性。首先获取窗口句柄(它在应用程序生命周期中不会改变),然后更新我们 WPF 预览处理程序的布局,使其适应控件的区域和边界。
class WPFPreviewHandler : ContentPresenter
{
IntPtr mainWindowHandle = IntPtr.Zero;
Rect actualRect = new Rect();
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == ContentControl.ActualHeightProperty | e.Property ==
ContentControl.ActualWidthProperty)
{
if (mainWindowHandle == IntPtr.Zero)
{
HwndSource hwndSource = PresentationSource.FromVisual(App.Current.MainWindow)
as HwndSource;
mainWindowHandle = hwndSource.Handle;
}
else
{
Point p0 = this.TranslatePoint(new
Point(),App.Current.MainWindow);
Point p1 = this.TranslatePoint(new Point(this.ActualWidth,this.ActualHeight),
App.Current.MainWindow);
actualRect = new Rect(p0, p1);
mainWindowHandle.InvalidateAttachedPreview(actualRect);
}
}
public static void InvalidateAttachedPreview(this IntPtr handler, Rect
viewRect)
{
if (pHandler != null)
{
RECT r = new RECT(viewRect);
pHandler.SetRect(ref r);
}
}
现在,我们唯一需要做的就是监听ContentProperty
的变化,并为显示的文件将预览处理程序附加到控件上
if (e.Property == ContentControl.ContentProperty)
{
mainWindowHandle.AttachPreview(e.NewValue.ToString(),actualRect);
}
我们完成了。最后一件要做的事情是在我们的COMStream
C# 类中实现IStream
接口,以便能够加载流式内容(例如,用于 PDF 预览器)
public sealed class COMStream : IStream, IDisposable
{
Stream _stream;
~COMStream()
{
if (_stream != null)
{
_stream.Close();
_stream.Dispose();
_stream = null;
}
}
private COMStream() { }
public COMStream(Stream sourceStream)
{
_stream = sourceStream;
}
#region IStream Members
public void Clone(out IStream ppstm)
{
throw new NotSupportedException();
}
public void Commit(int grfCommitFlags)
{
throw new NotSupportedException();
}
public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr
pcbWritten)
{
throw new NotSupportedException();
}
public void LockRegion(long libOffset, long cb, int dwLockType)
{
throw new NotSupportedException();
}
[SecurityCritical]
public void Read(byte[] pv, int cb, IntPtr pcbRead)
{
int count = this._stream.Read(pv, 0, cb);
if (pcbRead != IntPtr.Zero)
{
Marshal.WriteInt32(pcbRead, count);
}
}
public void Revert()
{
throw new NotSupportedException();
}
[SecurityCritical]
public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
{
SeekOrigin origin = (SeekOrigin)dwOrigin;
long pos = this._stream.Seek(dlibMove, origin);
if (plibNewPosition != IntPtr.Zero)
{
Marshal.WriteInt64(plibNewPosition, pos);
}
}
public void SetSize(long libNewSize)
{
this._stream.SetLength(libNewSize);
}
public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG
pstatstg, int grfStatFlag)
{
pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
pstatstg.type = 2;
pstatstg.cbSize = this._stream.Length;
pstatstg.grfMode = 0;
if (this._stream.CanRead && this._stream.CanWrite)
{
pstatstg.grfMode |= 2;
}
else if (this._stream.CanWrite && !_stream.CanRead)
{
pstatstg.grfMode |= 1;
}
else
{
throw new IOException();
}
}
public void UnlockRegion(long libOffset, long cb, int dwLockType)
{
throw new NotSupportedException();
}
[SecurityCritical]
public void Write(byte[] pv, int cb, IntPtr pcbWritten)
{
this._stream.Write(pv, 0, cb);
if (pcbWritten != IntPtr.Zero)
{
Marshal.WriteInt32(pcbWritten, cb);
}
}
#endregion
#region IDisposable Members
public void Dispose()
{
if (this._stream != null)
{
this._stream.Close();
this._stream.Dispose();
this._stream = null;
}
}
#endregion
}
现在我们完成了。我们可以使用非托管的预览处理程序来显示 WPF 应用程序中保存的文件的内容。此外,如果您愿意,可以创建自己的预览处理程序,它们将出现在您的 WPF 应用程序中,就像它们会自动出现在 Outlook 中一样。
历史
- 2008年4月22日:初始发布