Creating File Viewers in Windows 95

Nancy Winnick Cluts
Microsoft Developer Network Technology Group

January 1995

Abstract

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:

What Are 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

How File Viewers Work

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.

QUIKVIEW.EXE

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."

Registering File Viewers with the System

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.

Structure of Registry Entries

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.

Threading Models

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:

Example of Registering a File Viewer

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 Viewer Interfaces

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

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

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:

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

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:

Basic Steps in Creating a File Viewer

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.

  1. Define the file viewer object to implement the IPersistFile and IFileViewer interfaces.

  2. Implement the GetClassID, Load, and GetCurFile member functions (as well as the IUnknown members) of the IPersistFile interface.

    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.

  3. Implement the IFileViewer::ShowInitialize and IFileViewer::Show member functions (as well as the IUnknown members of IFileViewer).

    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;
    }
    
  4. Define the class factory object with the IClassFactory interface and implement that interface completely to create a file viewer object.

    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;
    }
    
  5. Implement the DllGetClassObject function to instantiate the class factory and return a pointer to one of its interfaces.
    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;
    
    }
    
  6. Implement the DllCanUnloadNow function to return the appropriate code depending on the number of file viewer objects in service and the number of lock counts affected through IClassFactory::LockServer.
    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);
    }
    
  7. (Optional) Implement the Print To feature by implementing IFileViewer::PrintTo. If this feature is not implemented, this member function must return ResultFromScode(E_NOTIMPL).

  8. Finish the DLL implementation with the DllEntryPoint function as required by any Win32 DLL.
    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.

Summary

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.