Embed Progress Bars in a List Control

Environment: VC6

Description:

This control can be used for displaying the progress of multiple events simultaneously. Each line in a list control can have its own progress bar. Progress bars operate independently and (can be) simultaneously.

This is also a very simple example of how to embed one control inside another by making the embedded control a child of the main window.

How to Use:

Create a list control on a dialog and make the variable of type CProgressList. For each item inserted into the list, you may call CreateProgress(ItemIndex) (not all items have to have a progress bar). This inserts the progress bar for that item. Then you can call SetProgress(ItemIndex). You must also add the CAdvHeaderCtrl class to your project.

Implementation:

I created a new MFC extension class derived from CListCtrl. Then added a method to create some static columns (for testing purposes), and a method to insert a couple items.

To create the progress bars, I added a method to create a progress bar for the specified index (passed in). This method creates an instance of a nested class that contains the index, sub-index, and CProgressCtrl members. After creating the CProgressEntry (the nested class), you have to call Create on the CProgressCtrl member to pass the progress control the style (which must include WS_CHILD), the parent (this-the ptr to the list control itself), and the window coordinates to occupy. The window coordinates are going to be exactly the same as the bounding rectangle of the specified index/subitem, so I used this->GetSubItemRect(item, subitem, Rect) to get the coordinates for the subitems square. Then I passed this rectangle into the progress controls create function.

void CProgressListCtrl::CreateProgress(int Index)
{
  // can only create progress for an existing item
  if (Index >= GetItemCount())
     return;

  // CProgressEntry is a nested class containing Index,
  // SubIndex, and CProgressCtrl members.

  CProgressEntry* ProgEntry = new CProgressEntry(Index, 2);
  CRect ItemRect;
  GetSubItemRect(Index, ProgEntry->m_SubIndex, LVIR_BOUNDS, 
                                ItemRect);
  int left = ItemRect.left;
  int top = ItemRect.top;
  int right = ItemRect.right;
  int bottom = ItemRect.bottom;
  (ProgEntry->m_Prog).Create(PBS_SMOOTH | WS_CHILD | WS_VISIBLE, 
                     CRect(left, top, right, bottom), this, 1);
  (ProgEntry->m_Prog).SetRange(0, 100);
  (ProgEntry->m_Prog).SetPos(0);
  m_ProgEntries[Index] = ProgEntry;
}

Here is the code for setting the progress bar for an item:

void CProgressListCtrl::SetProgress(int Index, int prog)
{
  CProgressEntry* ProgEntry;
  if (m_ProgEntries.Lookup(Index, ProgEntry) == TRUE)
     (ProgEntry->m_Prog).SetPos(prog);
}

Now the progress control is connected to the list control (through parent/child), and it is created in the right place. However, when the user scrolls the window, or moves/resizes it, we must tell the progress control the new coordinates to occupy. So I added some message handlers for Horizontal scroll, Vertical scroll, and move to get the new sub item rectangle and pass it to the Progress Ctrl using MoveWindow. All message handlers call ResizeProg().

void CProgressListCtrl::ResizeProg()
{
  CRect ItemRect;
  CProgressEntry* ProgEntry=0;
  int Index=0;
  POSITION pos = m_ProgEntries.GetStartPosition();
  while (pos != NULL) {
    m_ProgEntries.GetNextAssoc(pos, Index, ProgEntry);
    GetSubItemRect(ProgEntry->m_Index, ProgEntry->m_SubIndex, 
                                  LVIR_BOUNDS, ItemRect);
    int left = ItemRect.left;
    int top = ItemRect.top;
    int right = ItemRect.right;
    int bottom = ItemRect.bottom;
    (ProgEntry->m_Prog).MoveWindow(left, top, 
                       (right - left), (bottom - top));
  }
}

What about re-sizing the columns? This was a bit tricky. I had to use a class derived from CHeaderCtrl (CAdvHeaderCtrl), and hook the header to the ListCtrl. This header control was borrowed from Matt Weagles "Using the Header Control" example on www.codeproject.com. The message we need is HDN_ENDTRACK-it is sent when the user is done dragging the column divider (resizing the column). But it is sent to the list control header, NOT to the list control. So I made my new header class handle this message and call a function on the list ctrl (it could also send a message) to tell the list control to resize the progress bars (and move them).

void CAdvHeaderCtrl::OnEndTrack(NMHDR * pNMHDR, 
                                LRESULT* pResult)
{
  NMHEADER *pHdr = (NMHEADER*)pNMHDR;
  ((CProgressListCtrl*)GetParent())->ResizeProg();
  *pResult = 0;
}

To hook up the header, I created a member variable inside the list control class of type derived header. Then call (from somewhere in your initialization code I called it from CreateColumns)

m_Header->Init(GetHeaderCtrl())

This passes a pointer to the default header control into the new derived header ctrl class. The Init function simply SubClasses the default header control and voila, it is all hooked up. Now the new header can receive the column resize message (HDN_ENDTRACK) and tell the parent (the list control).

Clean up:

The progress entries (each progress control-the instance of the nested class) are stored in a CMap. In the list control destructor, I simply iterate the map and delete all the objects.

Other Notes:

This project was created fairly quickly, so it doesnt have extensive error handling or testing in other environments (document/view setting).

Also, there is some COM Automation code in the project that I was playing with. It works (kinda) if you want to play with it, but its not great. If you create a COM client to control this dialog app, then call Start, it will never return to the COM client (because it isnt multithreaded; Start uses ClearMsgQueue to keep the GUI active). But, if you click Start on the server app, and use the COM client to Stop it, it will work fine (because Stop returns right away). To fix it, youd have to make the progress bars run in a new thread (in the dialog class). Then Start would kick off a thread and return immediately so the COM client would be happy. E-mail me if you want to check out my sample COM client (its a C++ MFC COM project).

Downloads

Download demo project - 12 Kb
Download source 19 Kb