Back to the WFC main page

WFC Technical Note 5 - How to Create Explorer-like Applications in VC6

$Revision: 1 $ Last Modified $Date: 9/06/99 6:12p $

Introduction

Visual C++ 6.0 makes it easy to create user interfaces. Here's how to create an Explorer like application.

Steps

Click on the following:
  1. Visual C++
  2. File -> New
  3. Projects -> MFC AppWizard (exe)
  4. Fill in the Project Name and Location
  5. OK
  6. Single Document -> Next>
  7. None -> Next>
  8. None -> Next>
  9. Next>
  10. Next>
  11. You should now be at Step 6 of 6. Edit the class names and filenames as you see fit.
  12. Finish
  13. OK
  14. After Visual C++ generates a ton of C++ code, edit the CMainFrame class (usually found in MainFrm.h) to include a member variable:
    public:
    
       CSplitterWnd m_SplitterWindow;
  15. Don't worry about this member variable for now, just save the file.
  16. Now click on View -> ClassWizard -> Message Maps -> Add Class -> New...
  17. Name: CMyListView
  18. Base Class: CListView
  19. OK
  20. Add Class -> New...
  21. Name: CMyTreeView
  22. Base Class: CTreeView
  23. OK
  24. Select CMainFrame in the Class name: combobox.
  25. Select OnCreateClient in the Messages: listbox.
  26. Click the Add Function button.
  27. Double click on the OnCreateClient line in the Member functions: listbox.
  28. You will now be in the editor.
  29. Replace this line:
    return CFrameWnd::OnCreateClient(lpcs, pContext);
    with the following code:
    m_SplitterWindow.CreateStatic( this, 1, 2, WS_CHILD | WS_VISIBLE );
    
    CWnd * desktop_window_p = GetDesktopWindow();
    
    CRect rectangle;
    
    if ( desktop_window_p == NULL )
    {
       rectangle.SetRect( 0, 0, 640, 480 );
    }
    else
    {
       desktop_window_p->GetWindowRect( rectangle );
    }
    
    SIZE size;
    
    // Make the tree control pane 25% of the window size
    size.cx = (long) ( rectangle.Width() * 0.25 );
    size.cy = rectangle.Height();
    
    m_SplitterWindow.CreateView( 0, 0,
                                 RUNTIME_CLASS( CMyTreeView ),
                                 size,
                                 pContext );
    
    // Make the list control 75% of the window size
    size.cx = rectangle.Width() - size.cx;
    
    m_SplitterWindow.CreateView( 0, 1,
                                 RUNTIME_CLASS( CMyListView ),
                                 size,
                                 pContext );
    
    SetActiveView( (CView *) m_SplitterWindow.GetPane( 0, 0 ) );
    
    return( TRUE );
  30. Go to the top of the CMainFrame.cpp file (or whatever filename has CMainFrame in it) and find the line that looks like:
    #include "CMainFrame.h"
    Now add these lines immediately after it:
    #include "MyTreeView.h"
    #include "MyListView.h"
  31. Edit the stdafx.h file and delete the following lines:
    #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers
    #ifndef _AFX_NO_AFXCMN_SUPPORT
    #endif // _AFX_NO_AFXCMN_SUPPORT
    Now add this line to the rest of the includes:
    #include <afxcview.h>
  32. Build -> Rebuild All
  33. Your program should compile and run. It is time to play with ClassWizard...

Adding Useful Things to the Tree and List

I hate the Doc/View model of MFC. Things seem to fall into place once you abandon that model. Let's get rid of that tired paradigm:
  1. Edit your document class and add two member variables.
    public:
    
       CMyTreeView * TreeView;
       CMyListView * ListView;
  2. Just above the declaration for your document class, add these two include directives:
    #include "MyTreeView.h"
    #include "MyListView.h"
    Documents and views are very incestuous. Each has to know way too much about the other in order to function. This means the document class is a great place to put information about all of the views of that document. As long as your document doesn't represent data, everything is fine. Just think of your document class as the guy that manages all of the views (a View Manager if you will). You can have pointers to data in the document class but I do not recommend putting any real data in there.

  3. Edit your tree view class to add the following:
    public:
    
       CImageList ImageList;
    
       HTREEITEM RootItem;
       HTREEITEM CurrentItem;
  4. Edit the resources of your project to add a bitmap resource called IDB_IMAGELIST. Make your grid settings 17 pixels high by 16 wide (via the Image->Grid Settings...) Use the darker purple on the color palette bar for your transparent color (the background color).
  5. Insert -> Resource...
  6. Bitmap -> New
  7. Image -> Grid Settings...
  8. Check the Pixel Grid checkbox
  9. Check the Tile Grid checkbox
  10. Width: 16
  11. Height: 17
  12. OK
  13. Now move the image border so there is only one row of images (should be three wide).
  14. Fill the background with the dark purple color.
  15. Using fonts, put a 1 in the first tile.
  16. Put a 2 in the second tile.
  17. Put a 3 in the third tile. You will overwrite these ugly numbers with pretty little drawings later on in the life of your project. This usually happens when a graphic artist gets hired by your company.
  18. Left button on IDB_BITMAP1 in the ResourceView pane to highlight it.
  19. Then right mouse on it and select Properties from the context menu
  20. Change the ID: to IDB_IMAGELIST then close the window
  21. Now is a good time to save your work.
  22. View -> ClassWizard -> Message Maps
  23. Select CMyTreeView in the Class name: combobox.
  24. Select CMyTreeView in the Object IDs: listbox.
  25. Select WM_CREATE in the Messages: listbox.
  26. Click the Add Function button.
  27. Double click on the OnCreate line in the Member functions: listbox.
  28. Replace the code:
    if (CTreeView::OnCreate(lpCreateStruct) == -1)
        return -1;
    
        // TODO: Add your specialized creation code here
    
        return 0;
    with this code:
    lpCreateStruct->style |= ( TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT );
    
    if ( CTreeView::OnCreate( lpCreateStruct ) == (-1) )
    {
       return( -1 );
    }
    
    MyDocument * document_p = GetDocument();
    
    ASSERT( document_p != NULL );
    
    document_p->TreeView = this;
    
    ImageList.Create( IDB_IMAGELIST, 16, 0, RGB( 255, 0, 255 ) );
    ImageList.SetBkColor( GetSysColor( COLOR_WINDOW ) );
    
    CTreeCtrl& tree_control = GetTreeCtrl();
    
    tree_control.SetImageList( &ImageList, TVSIL_NORMAL );
    
    UpdateWindow();
    
    RootItem = tree_control.InsertItem( "Root", IMAGE_ROOT, IMAGE_ROOT );
    
    return( 0 );
  29. At the top of this file, you will need to include your document's header file.
    #include "MyDocument.h"
  30. Edit the MyTreeView.h header file to start adding defined constants for the images in the tree control (the tiles in the image list).
    #define IMAGE_ROOT   0
    #define IMAGE_TILE_2 1
    #define IMAGE_TILE_3 2
  31. View -> ClassWizard -> Message Maps
  32. Select CMyListView in the Class name: combobox.
  33. Select CMyListView in the Object IDs: listbox.
  34. Select WM_CREATE in the Messages: listbox.
  35. Click the Add Function button.
  36. Double click on the OnCreate line in the Member functions: listbox.
  37. Replace the code:
    if (CListView::OnCreate(lpCreateStruct) == -1)
        return -1;
    
        // TODO: Add your specialized creation code here
    
        return 0;
    with this code:
       lpCreateStruct->style |= LVS_REPORT;
    
       if ( CListView::OnCreate( lpCreateStruct ) == (-1) )
       {
          return( -1 );
       }
    
       CMyDocument * document_p = (CMyDocument *) GetDocument();
    
       document_p->ListView = this;
    
       return( 0 );
  38. At the top of this file, you will need to include your document's header file.
    #include "MyDocument.h"
  39. You should now be able to compile and run your new Explorer-like application.

Making Life Easier

Now that we have the skeleton for an Explorer-like application, we need to add things to it. Here's a couple of functions I wrote to make life easier.

Tree View Helpers

Add this function to your tree view class:
public:

   void DeleteChildren( HTREEITEM parent_item );
Implemented thusly:
void CMyTreeView::DeleteChildren( HTREEITEM parent_item )
{
   CTreeCtrl& tree_control = GetTreeCtrl();

   HTREEITEM child_item = (HTREEITEM) NULL;

   child_item = tree_control.GetChildItem( parent_item );

   while( child_item != NULL )
   {
      tree_control.DeleteItem( child_item );
      child_item = tree_control.GetChildItem( parent_item );
   }
}

List View Helpers

I usually add a couple of helper functions to make formatting the list view easier.
public:

   BOOL AddColumn( LPCTSTR column_name,
                   int     column_number,
                   int     width    = (-1),
                   int     nSubItem = (-1),
                   int     mask = LVCF_FMT  | LVCF_WIDTH |
                                  LVCF_TEXT | LVCF_SUBITEM,
                   int     format = LVCFMT_LEFT );

   BOOL AddItem( int     row_number,
                 int     column_number,
                 LPCTSTR text,
                 int     image_index = (-1) );

   void Empty( void );
Implemented thusly:
BOOL CMyListView::AddColumn( LPCTSTR column_name,
                             int column_number,
                             int width,
                             int sub_item,
                             int mask,
                             int format )
{
   CListCtrl& list_control = GetListCtrl();

   LV_COLUMN list_view_column;

   list_view_column.mask    = mask;
   list_view_column.fmt     = format;
   list_view_column.pszText = (LPTSTR) column_name;

   if ( width == (-1) )
   {
      list_view_column.cx = list_control.GetStringWidth( column_name ) + 15;
   }
   else
   {
      list_view_column.cx = width;
   }

   if ( mask & LVCF_SUBITEM )
   {
      if ( sub_item != (-1) )
      {
         list_view_column.iSubItem = sub_item;
      }
      else
      {
         list_view_column.iSubItem = column_number;
      }
   }

   return( list_control.InsertColumn( column_number, &list_view_column ) );
}

BOOL CMyListView::AddItem( int     row_number,
                           int     column_number,
                           LPCTSTR text,
                           int     image_index )
{
   LV_ITEM list_view_item;

   list_view_item.mask     = LVIF_TEXT;
   list_view_item.iItem    = row_number;
   list_view_item.iSubItem = column_number;
   list_view_item.pszText  = (LPTSTR) text;

   if ( image_index != (-1) )
   {
      list_view_item.mask  |= LVIF_IMAGE;
      list_view_item.iImage = image_index;
   }

   CListCtrl& list_control = GetListCtrl();

   if ( column_number == 0 )
   {
      return( list_control.InsertItem( &list_view_item ) );   
   }
   else
   {
      return( list_control.SetItem( &list_view_item ) );
   }
}

void CMyListView::Empty( void )
{
   CListCtrl& list_control = GetListCtrl();

   list_control.DeleteAllItems();

   while( list_control.DeleteColumn( 0 ) )
   {
      ;
   }

   UpdateWindow();
}
  • That is all. You should be able to compile and run your application.

    A Generic Design

    For the rest of this tech note, let's assume I'm writing an XML explorer using the XML classes in WFC.

    Generally speaking, when the user selects branches in the tree view, you want the list view (I prefer report view) to show details of that item. This means you should add a function to the list view class to show the details of each type of item. We also need a way to associate tree items with objects that will be displayed in these functions. In general, the document class is responsible for loading the object (and sub objects). The tree view is then passed this object and populates the tree items. When the user selects a tree item, the tree view figures out (via mapping) what the object selected was and tells the list view to display it.

    The Document

    The "document" is supposed to represent the data portion of your program. The only problem with this idea is when data changes, the views of the data must be updated. Instead of thinking of the "document" as your data, think of it as the object that gets your data. In reality, you want your data to be independent of any views of it. Your data objects should be blissfully ignorant of the objects that are displaying it. Use CDocument as a view manager and data router. It gets told by the framework to load new data (via the OpenDocument method). I suggest putting a pointer to your real data-centric object in the document class. Here's the steps you need to take to add your own code to load a file:
    1. View -> ClassWizard -> Message Maps
    2. Select CMyDocument in the Class name: combobox.
    3. Select CMyDocument in the Object IDs: listbox.
    4. Select OnOpenDocument in the Messages: listbox.
    5. Click the Add Function button.
    6. Double click on the OnOpenDocument line in the Member functions: listbox.
    7. Add your code to load your data.

    The Tree View

    The tree view is responsible for handling the user expanding branches and clicking on leaves. We need a way to link what the user clicks on with an object that leaf represents. To do this, I use a technique where the image identifier is used to differentiate between branches. The images may look the same but they have different identifiers (i.e. they appear in different places in the image list). They are duplicated in the image list. Differing image identifiers provides uniqueness. Uniqueness is the key to simplicity. Keep in mind that multiple branches can represent the same objects. Another things to remember is that a DWORD value can be associated with each item in the tree control. I usually put a pointer to the object in that DWORD. Of course, this will break when a 64-bit version of NT comes out (possibly named Win64???).

    To map what the user click on to what it represents, do the following:

    1. Edit the MyTreeView.h header file (this is the one that contains the image identifiers IMAGE_xxx defines).
    2. Add an enumeration to the CMyTreeView class. These enumerations will represent the objects.
      class CMyTreeView : public CTreeView
      {
         public:
      
            enum _what_it_is
            {
               IT_IS_THE_ROOT = 1,
               IT_IS_AN_XML_DOCUMENT,
               IT_IS_AN_ELEMENT,
               IT_IS_A_LIST_OF_ATTRIBUTES,
               IT_IS_AN_ATTRIBUTE
            };
    3. Now add a method to CMyTreeView to convert image id's to what they represent:
      DWORD ImageToWhatItIs( DWORD image_id ) const;
      implemented thusly:
      DWORD CMyTreeView::ImageToWhatItIs( DWORD image_id ) const
      {
         switch( image_id )
         {
            case IMAGE_ROOT:
      
               return( IT_IS_THE_ROOT );
      
            case IMAGE_TILE_2:
      
               return( IT_IS_AN_XML_DOCUMENT );
      
            case IMAGE_TILE_3:
      
               return( IT_IS_AN_ELEMENT );
      
            default:
      
               return( 0 );
         }
      }
    Now we need to add some code to populate the tree. I usually write one method that the document class calls (usually from OnOpenDocument()) and other methods to add individual items. For example:
    void CMyTreeView::Populate( CExtensibleMarkupLanguageDocument * xml_p )
    {
       // Start with an empty tree
       DeleteChildren( RootItem );
    
       if ( xml_p == NULL )
       {
          return;
       }
    
       CTreeCtrl& tree_control = GetTreeCtrl();
    
       HTREEITEM document_item;
    
       document_item = tree_control.InsertItem( TEXT( "XML" ), IMAGE_TILE_2, IMAGE_TILE_2, RootItem );
       tree_control.SetItemData( document_item, (DWORD) xml_p );
    
       CExtensibleMarkupLanguageElement * element_p = xml_p->GetRootElement();
    
       AddElement( tree_control, document_item, element_p );
    }
    To make things easy (and keep them straight in my little head), I put a comment block at the top of the file to give me a nice map to what everything means.
    /*
    ** Image Identifier to What It Is to Item Data mappings
    **
    ** IMAGE ID     | WHAT IT IS            | GetItemData() returns
    ** -------------+-----------------------+------------------------------------
    ** IMAGE_ROOT   | IT_IS_THE_ROOT        |
    ** IMAGE_TILE_2 | IT_IS_AN_XML_DOCUMENT | CExtensibleMarkupLanguageDocument *
    ** IMAGE_TILE_3 | IT_IS_AN_ELEMENT      | CExtensibleMarkupLanguageElement *
    */
    Here's where you will explain how to handle change notifications.

    The List View

    Now that the tree view knows how to populate itself from data given to it by the document class, we need to add code to show details of the object the user clicked on. The tree view controls what view the list view will show.

    I AIN'T DONE YET.


    Samuel R. Blackburn