Nancy Winnick Cluts
Microsoft Developer Network Technology Group
January 1995
File viewers allow the user to view the contents of a file quickly without having to run the original application that created the file. A file viewer provides the user interface for viewing a file, including menu items, a toolbar, and a status bar. A file viewer can also implement additional functionality for further shell integration. This article covers the following topics associated with file viewers:
Let's say that you created a bunch of Microsoftョ Word documents on a day that your imagination was impaired, and the files are named STUFF1.DOC, STUFF2.DOC, STUFF3.DOC遥ou get the picture. Now let's say that you want to give those files to someone who doesn't use Word. You can save those documents as .TXT files or, if that person happens to be running the Microsoft Windowsョ 95 operating system, that person can instead use a file viewer to view the contents of the files.
The Windows 95 shell has a new feature called Quick View that allows the user to quickly view the contents of a file without having to run the full application that created it and without even requiring the presence of that application. To view file contents, the user selects a file and chooses the Quick View menu item from the context menu of that selection, as shown in Figure 1 (or, the user can select Quick View from the File menu).
Figure 1. The context menu containing the Quick View option
Now before you go running off and trying this on your computer, be aware that if no file viewer exists for a particular file type, the Quick View option will not be displayed in the context menu for that file object. Windows 95 provides a default file viewer for many common file types, such as text files.
When a file viewer is created, it associates itself with file classes and extensions via the system registry. When the user clicks the right mouse button for an object in the file system, the shell checks the registry for a viewer for the object based upon the file class and extension. If no registry entry exists, the Quick View option is not displayed in the context menu.
A file viewer provides the user interface (menu items, toolbar, status bar, and so on) for viewing a file. The screen shot in Figure 2 shows the default file viewer that the system provides.
Figure 2. The default file viewer
In the above explanation about the Quick View option, I mentioned file classes. File viewers are OLE component objects implemented inside an in-process server DLL (dynamic-link library). When a user clicks the Quick View option, the shell uses the class identifier (an OLE CLSID value) of the file to determine which viewer to use if the file is an OLE compound file. If the file isn't a compound file, the shell uses the extension of the file to determine which viewer to use.
Since a file viewer is an OLE component object, you can add interfaces and additional functionality to support new features. For example, a file viewer can act as an OLE container application and can perform in-place activation of embedded objects inside the file being viewed. Another file viewer could be beefed up to allow the user to make a selection in a document and copy the selection to the Clipboard or source it in a drag-drop operation.
The shell doesn't directly call a file viewer; rather, it starts an instance of a program called QUIKVIEW.EXE for each file to be viewed. QUIKVIEW is a small program that directs the system to create a process and a message queue for each file viewer. It then associates a path with a file viewer, instantiates the file viewer object, and instructs the file viewer to load and display the file. QUIKVIEW turns over execution of the process to the file viewer until the file viewer shuts down.
QUIKVIEW uses three methods to determine a file's type so it can associate the file with the appropriate file viewer:
If there is no extension or if there are no file viewers registered for that extension, the Quick View operation fails and QUIKVIEW displays a message that reads "There are no viewers registered for <type of file> files."
If there is more than one file viewer possible for a file type listed in the registry, QUIKVIEW displays a dialog box containing a message that reads "Searching for a viewer to display or print the <human-readable document type> in <filename>. Press Cancel to stop the search." If the document type is known, the message reads "Searching for a viewer to display or print <filename>. Press Cancel to stop the search."
Since the shell determines which file viewer to call via a check in the registry, each file viewer must register certain information if it wants to be called. The best time to do this registration is during the installation of the file viewer. A file viewer can register itself for more than one file type if it can handle multiple file formats, but if a file type has more than one registered file viewer, the shell calls the most recently registered viewer for that file type when the user selects Quick View.
The following registry structure is required for QUIKVIEW to associate a class identifier or extension with the class identifier of a file viewer:
HKEY_CLASSES_ROOT
\QuickView
\<extension> = <human-readable document type>
\{<CLSID>} = <human-readable viewer name>
\{<CLSID>} = <human-readable viewer name>
\{<CLSID>} = <human-readable viewer name>
...[More extension entries for additional file types]
...
\CLSID
\{<CLSID>} = <human-readable viewer name>
\InprocServer32 = <full path to FileViewer DLL>
\ThreadingModel = <Model>
...[More class IDs for file viewers and other object servers]
In this registry structure:
Each class identifier stored under the file extension subkeys must correspond to an entry of that same class identifier stored under the top-level key, called CLSID. This is the standard location for storing information for OLE object servers. For file viewers, there must be an InprocServer32 subkey under the file viewer's class identifier key. The value of the InprocServer32 subkey is the full path to the file viewer DLL. InprocServer32 is a standard OLE subkey where the path to a component object server is stored. Using this subkey allows the QUIKVIEW program to use standard OLE APIs (application programming interfaces) to access and create objects from file viewer servers.
Under Windows 95, OLE is apartment threaded. "Apartment" is essentially just a way of describing a thread with a message queue that supports OLE/COM (Component Object Model) objects. Objects within an apartment are reentrant in only the traditional Windows sense, identical to single-threaded OLE. Operations that yield to the message queue can cause further messages to be sent to any objects within the apartment. Apartment model threading simply allows there to be more than one "apartment" where previously there was only one: the main application thread. By default, a single-threaded application consists of a single apartment (its single thread). When a process calls CoInitialize or OleInitialize from a thread, a new OLE apartment is created. Thereafter, each time CoInitialize or OleInitialize is called in a thread, a new OLE apartment is created.
Each OLE object exists within a single apartment or thread, and all calls into an object must occur while running in the object's apartment/thread. All users of the object in other threads must call the object through proxies. It is explicitly forbidden to call this object "directly" from a different apartment/thread. This is the fundamental, unbreakable, explicit rule about apartment model threading. Don't break it! I mean it.
In-process objects that are apartment-model-aware can be created in any apartment. You mark the DLL as apartment-model-aware via the ThreadingModel=Apartment value of the InprocServer32 key. In-process objects that are not apartment-model-aware are created in the main apartment of the application, the main apartment being the first thread that calls CoInitialize or OleInitialize.
An apartment-model-aware process must have thread-safe entry points because multiple apartments may be calling them to CoCreateInstance or CoGetClassObject simultaneously. In practice, this means that your application should do the following:
The following example demonstrates the registration of a file viewer for "C++ File" files (.CPL extensions). This sample file viewer, FILEVIEW, is provided in the Windows 95 Software Development Kit (SDK) with the Win32ョ samples. The file viewer is implemented in an in-process server DLL called FVTEXT.DLL. The DLL has the class identifier of 00021116-0000-0000-C000-000000000046. The actual registry entries appear in a file with the .REG extension. The .REG file is listed below. Note that the first line says "REGEDIT4". This specifies that the form of the registration file uses a new syntax. Also, note the syntax of each entry. There are square brackets ( [ ] ) around each key, and the value is enclosed in quotation marks. Use of this syntax allows the InprocServer32 and ThreadingModel subkeys to be specified. The threading model specified below is set to "Apartment". This value specifies that multiple threads in the executable can create OLE objects.
REGEDIT4
[HKEY_CLASSES_ROOT\.CPP]
@="C++ File"
[HKEY_CLASSES_ROOT\C++ File]
@="C++ Source File"
[HKEY_CLASSES_ROOT\C++ File\CLSID]
@="{00021116-0000-0000-C000-000000000046}"
[HKEY_CLASSES_ROOT\QuickView\{00021116-0000-0000-C000-000000000046}]
@="C++ Source File"
[HKEY_CLASSES_ROOT\QuickView\{00021116-0000-0000-C000-000000000046}\{00021117-
0000-0000-C000-000000000046}]
@="Sample Text Viewer"
[HKEY_CLASSES_ROOT\QuickView\{00021117-0000-0000-C000-000000000046}]
@="Sample Text Viewer"
[HKEY_CLASSES_ROOT\QuickView\.CPP]
@="C++ Source File"
[HKEY_CLASSES_ROOT\QuickView\.CPP\{00021117-0000-0000-C000-000000000046}]
@="Sample Text Viewer"
[HKEY_CLASSES_ROOT\CLSID\{00021117-0000-0000-C000-000000000046}]
@="Sample Text Viewer"
[HKEY_CLASSES_ROOT\CLSID\{00021117-0000-0000-C000-000000000046}\InprocServer32]
@="c:\\windows\\system\\viewers\\fvtext.dll"
"ThreadingModel"="Apartment"
File viewers use the IPersistFile interface to get the path for a file. From then on, the component that loaded the object can ask it to do any number of things with the file. In the future, the shell may ask the object to perform content indexing, which would then happen through an interface other than IFileViewer. For this reason, the file-loading member functions of IPersistFile are separate from the operations to perform on that file, which is why IFileViewer wasn't simply extended with its own Load member function.
The IFileViewer interface allows a registered file viewer to be notified when it must show or print a file. The Windows 95 shell calls this interface when the user selects Quick View from a file's context menu, and the file is a type that the file viewer recognizes. The interface identifier of IFileViewer is defined in the Windows header files by the IID_IFileViewer named constant. Like all OLE interfaces, IFileViewer also includes the QueryInterface, AddRef, and Release methods. The following methods are specific to IFileViewer.
IFileViewer::ShowInitialize
HRESULT STDMETHODCALLTYPE ShowInitialize(LPFILEVIEWERSITE lpfsi);
This method allows a file viewer to determine whether it can display a file and, if so, to perform initialization operations before showing a file. The shell calls this method before calling IFileViewer::Show. This method must perform all operations that are prone to failure so that, if this method succeeds, the IFileViewer::Show method will not fail. The shell specifies the name of the file to display by calling the file viewer's IPersistFile::Load method. This method returns NOERROR if successful, an OLE-defined error value otherwise. This method takes the following parameter:
The FVSHOWINFO structure contains information that the IFileView::Show method uses to display a file. The shell uses this structure to pass information to a file viewer, and a file viewer uses it to return information to the shell. It is defined as follows:
FVSHOWINFO
typedef struct {
DWORD cbSize; // size of this structure, in bytes
HWND hwndOwner; // handle of the owner window
int iShow; // how to show the file
DWORD dwFlags; // flags
RECT rect; // size and position of file viewer window
LPUNKNOWN punkrel; // release interface
OLECHAR strNewFile[MAX_PATH]; // new file to view
} FVSHOWINFO, *LPFVSHOWINFO;
In this structure:
FVSIF_CANVIEWIT: Indicates that the file viewer can display the file.
FVSIF_NEWFAILED: Indicates that the file viewer specified a new file to display, but no viewer could display the file. The file viewer should either exit or continue to display the previous file.
FVSIF_NEWFILE: Indicates that a drag-drop operation has dropped a file on the file viewer window. The file viewer passes the name of the file to the shell by copying the name to strNewFile. The shell attempts to load a file viewer that can display the new file.
FVSIF_PINNED: Indicates that a pinned window exists. A pinned window is a window in which the current file viewer is displaying a file. A file viewer should either use the pinned window to display the file, or set a new pinned window and display the file in it.
FVSIF_RECT: Indicates that rect contains valid data.
IFileViewer::Show
HRESULT STDMETHODCALLTYPE Show(LPFVSHOWINFO pvsi);
This method is used to display a file. The shell specifies the name of the file to display by calling the file viewer's IPersistFile::Load method. This method returns NOERROR if successful, or E_UNEXPECTED if IFileView::ShowInitialize was not called before IFileView::Show. This member function is similar to the Windows ShowWindow function in that it receives a Show command that indicates how the file viewer should initially display its window. Note that the Windows 95 shell always starts QUIKVIEW with SW_SHOWNORMAL. If a WM_DROPFILES message is processed by the window, the following fields of the FVSHOWINFO structure should be filled in:
This method takes the following parameter:
IFileViewer::PrintTo
HRESULT STDMETHODCALLTYPE PrintTo(LPSTR pszDriver, BOOL fSuppressUI);
This method prints a file. The shell specifies the name of the file to print by calling the file viewer's IPersistFile::Load method. This method returns NOERROR if successful, and an OLE-defined error value otherwise. This member function is like Show in that it does not return until it finishes printing or an error occurs. If there is a problem, the file viewer object is responsible for informing the user of the problem.
This method takes the following parameters:
The IFileViewerSite interface allows a file viewer to retrieve the handle of the current pinned window or to set a new pinned window. The pinned window is the window in which the current file viewer is displaying a file. When the user selects a new file to view, QUIKVIEW uses the pinned window as a way to communicate to the shell where to copy the file contents. The shell directs the file viewer to display the new file in the pinned window rather than create a new window. QUIKVIEW generates a WM_DROPFILES message to communicate with the shell when a file is dropped on the viewer. Only one window can have the pinned state at a time. To clear the pinned state for a window, the application uses the SetPinnedWindow method and passes a NULL for the HWND parameter. Like all OLE interfaces, IFileViewerSite also includes the QueryInterface, AddRef, and Release methods. The following methods are specific to IFileViewerSite.
IFileViewerSite::GetPinnedWindow
HRESULT STDMETHODCALLTYPE GetPinnedWindow(HWND *phwnd);
This method retrieves the handle of the current pinned window, if it exists. It returns NOERROR if successful, an OLE-defined error value otherwise.
This method takes the following parameter:
IFileViewerSite::SetPinnedWindow
HRESULT STDMETHODCALLTYPE SetPinnedWindow(HWND hwnd);
This method sets a new pinned window. When the user selects a new file to view, the shell directs the file viewer to display the new file in the pinned window instead of creating a new window. It returns NOERROR if successful, an OLE-defined error value otherwise.
This method takes the following parameter:
Implementing a file viewer to interact appropriately with QUIKVIEW involves approximately eight different steps. The Windows 95 SDK includes a sample, FILEVIEW, that demonstrates how to create a file viewer. This section lists each step and includes some sample code from FILEVIEW.
The IsDirty member function can simply return ResultFromScode(S_FALSE) because a file viewer does not modify the file. The Save and SaveCompleted member functions should return ResultFromScode(E_NOTIMPL). GetClassID returns the file viewer's class identifier. GetCurFile returns ResultFromScode(E_UNEXPECTED) if Load has not yet been called; otherwise, it copies the path and returns NOERROR. Load stores the filename, but doesn't open the file until the call to ShowInitialize.
ShowInitialize must perform all operations that are prone to failure such that, if ShowInitialize succeeds, Show will never fail. The following code snippet demonstrates an implementation of the ShowInitialize member. Some error reporting has been removed from this sample to save space.
STDMETHODIMP CFileViewer::ShowInitialize(LPFILEVIEWERSITE lpfsi)
{
HRESULT hr;
HMENU hMenu;
// Do pre-show initialization here.
if (m_lpfsi != lpfsi)
{
if (NULL!=m_lpfsi)
m_lpfsi->Release();
m_lpfsi = lpfsi;
lpfsi->AddRef();
}
//Default error code.
hr=ResultFromScode(E_OUTOFMEMORY);
//Create the main window passing "this" to it.
m_hWndOld = m_hWnd;
m_hWnd=CreateWindow(String(IDS_CLASSFRAME), String(IDS_CAPTION),
WS_MINIMIZEBOX | WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT , CW_USEDEFAULT, 350, 450, NULL, NULL, m_hInst,
(LPVOID)this);
if (NULL==m_hWnd)
return hr;
// Let us accept files.
DragAcceptFiles(m_hWnd, TRUE);
if (!FInitFrameControls())
return hr;
// Set initial view menu item checks.
hMenu=GetMenu(m_hWnd);
CheckMenuItem(hMenu, IDM_VIEWTOOLBAR, MF_BYCOMMAND | MF_CHECKED);
CheckMenuItem(hMenu, IDM_VIEWSTATUSBAR, MF_BYCOMMAND | MF_CHECKED);
m_fToolsVisible=TRUE;
m_fStatusVisible=TRUE;
m_pSH->MessageDisplay(ID_MSGREADY);
// ViewportResize puts the viewport window created here
// in the right location, so we don't have to worry
// about initial position.
m_hWndViewport=CreateWindowEx(WS_EX_CLIENTEDGE,
String(IDS_CLASSVIEWPORT), "Viewport",
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL,
0, 100, 100, m_hWnd, (HMENU)ID_VIEWPORT,
m_hInst, (LPVOID)this);
if (NULL==m_hWndViewport)
return hr;
// Resize the viewport.
ViewportResize();
// Load the file.
hr=FileLoad();
if (FAILED(hr))
return hr;
// Load the accelerators.
m_hAccel=LoadAccelerators(m_hInst, MAKEINTRESOURCE(IDR_ACCELERATORS));
// Tell IFileViewer::Show it's OK to call it.
m_fShowInit=TRUE;
return NOERROR;
}
The Show member displays the contents of that file in the viewport window, shows the top-level file viewer window, and enters a message loop. The following code snippet demonstrates an implementation of the Show method.
STDMETHODIMP CFileViewer::Show(LPFVSHOWINFO pvsi)
{
MSG msg;
// If ShowInitialize failed, set the hwnd back to the old hwnd.
if ((pvsi->dwFlags & FVSIF_NEWFAILED) && (m_hWnd == NULL))
m_hWnd = m_hWndOld;
if (!IsWindow (m_hWnd))
return ResultFromScode(E_UNEXPECTED);
m_pvsi = pvsi;
// If the new failed flag was passed to us we know that we got here
// because we tried to view a file and it failed, so simply go back
// to message loop.
if ((pvsi->dwFlags & FVSIF_NEWFAILED) == 0)
{
if (pvsi->dwFlags & FVSIF_RECT)
SetWindowPos(m_hWnd, NULL, pvsi->rect.left, pvsi->rect.top,
pvsi->rect.right - pvsi->rect.left, pvsi->rect.bottom - pvsi->rect.top,
SWP_NOZORDER | SWP_NOACTIVATE);
ShowWindow(m_hWnd, pvsi->iShow);
if (SW_HIDE!=pvsi->iShow)
{
SetForegroundWindow(m_hWnd);
UpdateWindow(m_hWnd);
}
// If there is an old window, destroy it now.
if (pvsi->dwFlags & FVSIF_PINNED)
{
m_lpfsi->SetPinnedWindow(NULL);
m_lpfsi->SetPinnedWindow(m_hWnd);
HMENU hMenu=GetMenu(m_hWnd);
CheckMenuItem(hMenu, IDM_VIEWREPLACE, MF_BYCOMMAND|MF_CHECKED);
}
if (SW_HIDE!=pvsi->iShow)
UpdateWindow(m_hWnd);
if ((NULL!=m_hWndOld) && IsWindow(m_hWndOld))
{
m_fPostQuitMsg = FALSE; // don't destroy the queue for this one
DestroyWindow(m_hWndOld);
m_hWndOld = NULL;
}
if (NULL!=pvsi->punkRel)
{
pvsi->punkRel->Release();
pvsi->punkRel = NULL;
}
}
while (GetMessage(&msg, NULL, 0,0 ))
{
if (!TranslateAccelerator(m_hWnd, m_hAccel, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// If there is a new file, bail out now.
if (m_pvsi->dwFlags & FVSIF_NEWFILE)
break;
}
// Perform cleanup here.
return NOERROR;
}
The following code is used to instantiate a file viewer object.
STDMETHODIMP CFVClassFactory::CreateInstance(LPUNKNOWN pUnkOuter,
REFIID riid, PPVOID ppvObj)
{
PCFileViewer pObj;
HRESULT hr;
*ppvObj=NULL;
hr=ResultFromScode(E_OUTOFMEMORY);
// Verify that a controlling unknown asks for IUnknown.
if (NULL!=pUnkOuter && !IsEqualIID(riid, IID_IUnknown))
return ResultFromScode(E_NOINTERFACE);
// Create the object passing function to notify on destruction.
pObj=new CFileViewer(pUnkOuter, g_hInst, ObjectDestroyed);
if (NULL==pObj)
return hr;
hr=pObj->Init();
if (SUCCEEDED(hr))
{
// Return the requested interface.
hr=pObj->QueryInterface(riid, ppvObj);
if (SUCCEEDED(hr))
{
g_cObj++;
return NOERROR;
}
}
// Delete the object.
delete pObj;
return hr;
}
HRESULT PASCAL DllGetClassObject(REFCLSID rclsid, REFIID riid, PPVOID ppv)
{
// Ensure that OLE is initialized.
OleInitialize(NULL);
if (!IsEqualCLSID(rclsid, CLSID_FileViewerText))
return ResultFromScode(E_FAIL);
// Check that we can provide the interface.
if (!IsEqualIID(riid, IID_IUnknown)&& !IsEqualIID(riid, IID_IClassFactory))
return ResultFromScode(E_NOINTERFACE);
// Return our IClassFactory for our viewer objects.
*ppv=new CFVClassFactory();
if (NULL==*ppv)
return ResultFromScode(E_OUTOFMEMORY);
// AddRef the object through any interface we return.
((LPUNKNOWN)*ppv)->AddRef();
return NOERROR;
}
STDAPI DllCanUnloadNow(void)
{
SCODE sc;
// Our answer is whether there are any objects or locks.
sc=(0L==g_cObj && 0L==g_cLock) ? S_OK : S_FALSE;
return ResultFromScode(sc);
}
extern "C" BOOL WINAPI LibMain(HINSTANCE hInstance, ULONG ulReason,PVOID
pvReserved)
{
if (DLL_PROCESS_DETACH==ulReason)
return TRUE;
else
if (DLL_PROCESS_ATTACH!=ulReason)
return TRUE;
g_hInst=hInstance;
return TRUE;
}
In general, only the implementations of IPersistFile::Load and the IFileViewer member functions are specific to a file viewer. The other steps that deal with creating an OLE component object are standard OLE mechanisms.
This article is a basic overview of file viewers in Windows 95. There is also information in the Windows 95 SDK under the File Viewer Help topic. After reading this article and taking a look at the FILEVIEW sample, you too should be able to create your own file viewer. Although I talk only about simple file viewer capabilities, there isn't any reason why a file viewer couldn't have more useful features, such as drag-and-drop or Clipboard support. I'm sure that there are also some other useful features that don't come to mind right now, but I've never claimed to have the most active imagination.