Introduction
First of all, what is a preview handler? Preview handler is a COM object, that is called when you want to display the preview of your item. In other words, preview handlers are lightweight, rich and read-only previews of file’s content in a reading pane. You can find preview handlers in Microsoft Outlook 2007, Windows Vista and, even sometimes in XP. Can we use preview handlers within our WPF application? Probably we can. Let’s see how we can do it.
Let's Do It
Let's create a simple WPF window that displays a file list from the left and preview of items on the right side. We'll use a simple file list string
collection as our datasource, bind it to Listbox
Items and then bind the selected item to some contentpresenter
. I blogged about this approach earlier.
<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>
Our data source should be updated automatically with changes of the file system. So, this is a very good chance to use the FileSystemWatcher
object.
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);
}
}
Now, after applying a simple DataTemplate
, we can see the file list in the left pane of our application. It will be updated automatically upon file change in a certain directory.
The next step is to understand how to use preview handlers within a custom application. After all, a preview handler is a regular COM object that implements the following interfaces:
[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);
}
In order to find and attach the preview handler to a specific file type, all we have to do is to look into HKEY_CLASSES_ROOT
and find COM Guid of the preview handler (8895b1c6-b41f-4c1c-a562-0d564250836f
). The default value of this key will be the Guid of COM object, that actually can preview this type of file. Let's do it:
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());
Now, we know that this file can be previewed. Thus let's initialize the appropriate COM instance for the preview handler:
Type a = Type.GetTypeFromCLSID(g, true);
object o = Activator.CreateInstance(a);
There are two kinds of initializations for preview handlers – file and stream based. Each one has its own interface. So, we can only check if the object created implements this interface to be able to initialize the handler.
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;
}
After we initialized the handler, we can set a handle to the window we want the handler to sit in. We should also provide bounds of region of the window to the handler to be placed in.
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();
}
}
So far so good, but we're in WPF. Thus, the ContentPresenter
we're using has no handle! That's right, but the main WPF application window has. So, let's first get the main application window handle, then create rectangle bounds of the region, occupied by ContentControl
.
In order to do it, we'll derive from ContentPresenter
and listen to its ActualHeight
and ActualWidth
properties. First get the window handler (it won't be changed during the application life cycle), then update the layout of our WPF preview handler for the region and bounds of the control.
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);
}
}
Now, the only thing we have to do is to listen for ContentProperty
change and attach the preview handlers for the displayed file to the control:
if (e.Property == ContentControl.ContentProperty)
{
mainWindowHandle.AttachPreview(e.NewValue.ToString(),actualRect);
}
We're done. The last thing to do is to implement the IStream
interface in our COMStream
C# class in order to be able to load streaming content (for example, for the PDF previewer):
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
}
And now we're finished. We can use unmanaged preview handlers to display content of our files, held by the WPF application. Also, if you want, you can create your own preview handlers and they'll appear in your WPF application as well as they'll magically appear in Outlook.
History
- 22nd April, 2008: Initial post