Enhanced List Control

Download demo with formview and dialog 534K

Download Dao dynamic link list in a formview 635

Download GfxListCtrl as a CListView (unsupported) 373K

the list control (14 K)
This is an enhanced version of the list control; it include some features from other Codeguru's articles. I would especially name Zafir's articles and Sam Fugarino's (his article enlightened me about database & list controls). However, a lot of code comes from other people, too much to be all named here. So please don't say I stole code! I put it here and everybody can take what needed from it.
This control has the following features:
  • entire row highlight
  • automatic sorting of column (using numeric and text sort)
  • support for background images and blend highlight selection
  • support for vertical and horizontal grid
  • draggable column
  • customizable column format (size, alignment) with format dialog box
  • management of column using Column Manager
  • support for a "categorized view" (like Outlook 98)
  • support, on every cell, for custom text color, font and grid borders
  • automatic saving and loading from registry of column status
  • print support
  • support for images in cell
  • support for images in header
  • use a richedit derived control for editing of subitems
  • support of subitem editing using a combobox-like control
  • automatic copying of items in clipboard
  • support for showing scroll bar always
  • custom items height (not by single items)
  • cell tooltips
  • not resizeable columns
  • highlight of currently selected cell
  • automatic bitmapped popup menus for copy/paste operation in richedit controls and column format/customize
  • support for dinamic Dao database link and link of list subitems with other recordsets with combobox select
  • *new* support for item autopreview
  • *new* (un)support for CListView

1. The list control

The control has been designed for use with the LPSTR_TEXTCALLBACK for inserting items. It can be used even with normal text insert, but column manager, column dragging and view by categories are supported only through use of LPSTR_TEXTCALLBACK. The customized text color and fonts, and the categories handling are managed using callback functions; that means you have to write some functions in the list parent class (in the demo, a form view) to handle those features.

2. Creating the list control

If the control is being created from a form view or a dialog, you'll have to manually call this function (assuming the control is wndList):
wndList.OnInit();
This because the WM_CREATE will not be sent when the control is "auto" created; it is not necessary if it is manually created using the standard Create() function. After creating, you can define the internal styles of the control using the ModifyFlag() function; those are the flags you can use:

  • fHighRowSelect - selection of full row
  • fVertGrid - enable vertical grid lines
  • fHorizGrid - enable horizontal grid lines
  • fAutoSort - auto sorting on columns
  • fExInfo - enable custom colors and fonts for cells
  • fSubItemImages - enable images in cells
  • fAllowMultiChange - enable to extend the changes made to an item to all the items actually selected
  • fAllowDivider - enable custom grid lines
  • fCellToolTip - enable cells tooltip
  • fExInfoOnRow - the custom colors and fonts are to be asked for line and not for cell (faster than for cell)
  • fReturnDblClk - the enter key will be handled has a left mouse button double click on selected item
  • fReturnModify - if the currently selected cell supports editing, the enter key will start the editing
  • fScrollBarAlways - show the scroll bars always (gives a better look if WS_EX_STATICEDGE is selected)

By default, those flags are setted in the list control constructor: fHighRowSelect, fVertGrid, fHorizGrid, fAutoSort, fExInfo, fSubItemImages, fCellToolTip, fReturnModify, fReturnDblClk, fScrollBarAlways.

The ModifyFlag has the same syntax of CWnd::ModifyStyle(flag_to_be_removed, flag_to_be_added, redraw_flag).

3. The Column Manager

The list control implements a Column Manager; this permits column customize and column dragging. To use this feature we need to first define all the columns we can have, then specifing the columns we want to be shown and finally setting them up.

  • a. defining the columns we can have
    Usually done in the view OnInitialUpdate or in the dialog OnInitDialog, the definition of the columns is done with the function DefineColumn(). This function as the following syntax:
    int DefineColumn (const int iId, const char * cText, const char * cTextDt, const int iAlign, const int iWidth, DWORD dwData, const int iImage, const char * cDescr)
    iId is the identifier of the column, cText is the description of the column, cTextDt is here for internal use only - set it to NULL, iAlign is the alignment of the column as normally defined for list controls - LVCFMT_LEFT, LVCFMT_CENTER, .., iWidth is the default width of the column (don't use -1, put a value instead), dwData are the flags of the column as defined below, iImage is the index of the column image if any, cDescr is the description of the column if it is an image column.
    This function is from the class CGfxColumnManager; the CGfxColumnManager object is created and handled by the list control. You can have access to it using:
    CGfxColumnManager * pManager = wndList.CreateColumnManager();
    (for the first time you access it) and
    CGfxColumnManager * pManager = wndList.GetColumnManager();
    elsewhere.
    • fhComboBox - cells of column are editables using a "droplist combobox"
    • fhComboBoxEx - cells of column are editables using a "dropdown combobox" (editable)
    • fhEdit - cells of column are editables using a richedit control
    • fhNumeric - cells of column contain numeric values (for sorting)
    • fhValute - cells of column contain valute values (for sorting - means in format "1.199.990")
    • fhSortAble - enable automatic sort for this column
    • fhImage - the cells of this column contain images
    • fhColumnHasImage - the column contain an image instead of text
    • fhNoSortArrow - the sort order arrow will not be draw
    • fhNoResizeColumn - column has fixed width, user cannot resize it


    If we are using images for header we should setup an image list containing them; we have to create an image list and adding it to the listcontrol using the SetHeaderImageList() function; it has this syntax:
    void SetHeaderImageList(CImageList * pIma);

    If we are using images for list items, we have to do the same thing has above using the SetItemImageList() function:
    void SetItemImageList(CImageList * pIma);

    When using this control, you should avoid the standard imagelist linked to list control; they should work but can cause troubles with draggable column.
    If we want to use the column dragging, we have to insert the first column as below:
    pManager->DefineColumn(0, "", NULL, LVCFMT_LEFT, 0, fhNoSortArrow|fhNoResizeColumn);
    The column id will be used when retrieving data about user fonts, colors, text of items and so on. The functions requiring those data will pass the column id you insert when defining columns.
    Here's an example of how we can define some columns:
    pManager->DefineColumn(0, "", NULL, LVCFMT_LEFT, 0, fhNoSortArrow|fhNoResizeColumn);
    pManager->DefineColumn(1, "Column 0");
    pManager->DefineColumn(2, "Column 1", NULL, LVCFMT_CENTER, 120, fhEdit|fhNumeric|fhSortAble);
    pManager->DefineColumn(3, "Column 2", NULL, LVCFMT_LEFT, 220, fhEdit|fhSortAble);
    pManager->DefineColumn(7, "Column 3", NULL, LVCFMT_LEFT, 120, fhEdit|fhSortAble);
    pManager->DefineColumn(4, NULL, NULL, LVCFMT_LEFT, 20, fhImage|fhNoSortArrow|fhNoResizeColumn|fhSortAble|fhColumnHasImage, 0, "Image 1");
    pManager->DefineColumn(5, NULL, NULL, LVCFMT_LEFT, 20, fhImage|fhNoSortArrow|fhNoResizeColumn|fhSortAble|fhColumnHasImage, 1, "Image 2");
    pManager->DefineColumn(6, NULL, NULL, LVCFMT_LEFT, 20, fhImage|fhNoSortArrow|fhNoResizeColumn|fhSortAble|fhColumnHasImage, 2, "Image 3");
    pManager->DefineColumn(8, "A Combobox column", NULL, LVCFMT_LEFT, 120, fhComboBoxEx|fhSortAble);

  • b. defining the columns we want to show
    This list control supports customizable column; so it support a system to save/load the column state, reflecting the customization (from simple alignment changes to column position, size and on/off state). We define a "standard" status for the column providing an array of integers containing the id of the column we want to show and their order, like:
    int _DEFCOLS[] = { 0,2,4,5,6,3,1,8,7 };
    pManager->DefineDefaultColumns(sizeof(_DEFCOLS)/sizeof(int), _DEFCOLS);

    That means we want to show the column with ids 0,2,4, etc.. in the order we provide them in _DEFCOLS (which doesn't necessary reflects the order in which we defined them).
    If we want to use the save/load system, after we defined the standard state, we can call the ReadFromProfile(). This function has the following syntax:
    pManager->ReadFromProfile("ListCtrlName");
    This function retrieve from the registry the status of the column. "ListCtrlName" is a name we use to identify the list control customization; we can support customization for different list control inside the same application providing different names here.
    The function which saves the status of the list control into the registry is WriteToProfile() which takes the same parameter as ReadFromProfile, the name of the list control. A good place to use WriteToProfile is the OnDestroy of the dialog/view, like:
    void CMyView::OnDestroy()
    {

      // If we are in category view, we need to switch back to normal view before saving the state (or column 0 will have incorrect size)
      if (wndList.GetCategoryManager()) wndList.EnableCategoryView(false);
      CGfxColumnManager * pManager = wndList.GetColumnManager();
      // We have to call RetrieveColumnData before WriteToProfile
      pManager->RetrieveColumnData(&wndList);
      pManager->WriteToProfile("TheMainList");
      // We need to do the save operations before the calling to OnDestroy()
      CFormView::OnDestroy();

    }


    If we are not interested in saving and loading the state of columns, we can simply avoid the ReadFromProfile and WriteToProfile functions. The DefineDefaultColumns has to be called.

  • c. setting up the columns
    After we have defined the columns and the standard state (and eventually load the saved state from registry), we can call the SetupColumnData() function of the list control to setting up the columns:
    wndList.SetupColumnData();
    After that, we can begin the inserting of items.

4. The callbacks functions

This list control doesn't store the information about custon colors and fonts; instead, it calls a callback function or send a message to its parent when it needs such information.
The support for categorized list require a callback function and it's not possible to choose between callback or message; the exinfo required for colors, fonts and editing info supports both callback or message (that means that if you provide a pointer to a callback function, the control will use it; otherwise, it will send a message WM_LISTEX_NOTIFY to its parent when custom information are needed.
There's also the support for a callback in text retriving format (the work normally done in LVN_GETDISPINFO messages); getting text through callback is faster than through LVN_GETDISPINFO message; but the control still send these messages sometimes, so if you want to provide a callback function for text retriving you will have to provide the LVN_GETDISPINFO too; sometimes the list control will call the callback, sometimes the LVN_GETDISPINFO; I usually give the callback and call the callback from the LVN_GETDISPINFO function.

The three callbacks are this:

  • a. The ExInfo callback (or the WM_LISTEX_NOTIFY message)
    This function is called when the control needs custom colors and fonts of list items, when the user updates an item and when the control needs a custom grid.
    We need to define a function in the parent class (a view or a dialog) a function with this declaration:
    long GetExInfoCallback(LXHDREX * pLx);
    if we choose to use messages instead of callback, we need to make a routine to handle the WM_LISTEX_NOTIFY message, like this:

    BEGIN_MESSAGE_MAP(CMyView, CFormView)
    	//{{AFX_MSG_MAP(CMyView)
    	...
    	//}}AFX_MSG_MAP
    	ON_MESSAGE(WM_LISTEX_NOTIFY, OnListExNotify)
    END_MESSAGE_MAP()
    
    

    LRESULT CMyView::OnListExNotify(WPARAM wParam, LPARAM lParam)
    {

      LXHDREX * pLx = (LXHDREX *) lParam;
      ASSERT(pLx);

    }
    The OnListExNotify() and the GetExInfoCallback() functions do the same thing; they have different parameters but you see above as the lParam of the OnListExNotify() if the same of the LXHDREX * pLx in the GetExInfoCallback(). The callback function is much faster than the message function, and you are encouraged to use this way. Simply define a function like:
    long GetExInfoCallback(LXHDREX * pLx);
    in the parent (let's say the view, but it can be every a CWnd derived object). You tell the list control to use this callback calling this function:
    wndList.SetExInfoCallback((long (CWnd::*)(LXHDREX *))GetExInfoCallback);

    The struct LXHDREX passed here has the following members:

    class LXHDREX
    {
    	int            iNotifyCode;
    	int            CtrlId;
    	int            iItem;
    	int            iSubItem;
    	DWORD          dwItemData;
    	DWORD          dwMask;
    	DWORD          dwFlag;
    	COLORREF       crText;
    	COLORREF       crBack;
    	HFONT          hfText;
    	CString        csText;
    	CStringArray * pComboArray;
    }
     iNotifyCode 

      contains a sub code, which can be:

    • NTEX_ITEMINFO - means you can customize the font and color of the cell; if you specify fExInfoOnRow in the control flag, this message will be sent only once per line; if you didn't specify it, it will be send every time a cell is draw. You'll have to set the dwMask, crText and hfText members of this structure to define the text visualization. Return 1 if some customization has been done, 0 for standard processing.
    • NTEX_COMBOREQUEST
    • - means user has clicked on a cell of a combobox column and wants to initiate edit. pComboArray will contain a valid pointer to an empty CStringArray; you can add to this array the members you want into the combobox. Return 1 if you added something to the combo box.

    • NTEX_SETTEXT - means the user has terminate the editing process of a cell (combobox or edit); the modified value will be in csText. If you return 1 from this message, the control will set the text of this item (using the SetItemText function); othervise the item will be set to LPSTR_TEXTCALLBACK.
    • NTEX_DIVIDER - send if you set the fAllowDivider flag in control flag. This permits you to customize the drawing of the grid. You can cast the LXHDREX pointer to a LXHDREX_DIV pointer. The LXHDREX_DIV class contains the same members plus:
    • class LXHDREX_DIV : public LXHDREX
      {
      public:
      	CDC *	pDC;
      	CRect	rcItem;
      };
      

      Those members will contain the CDC member to draw the grid and the bounding rectangle of the item. Return 1 if the standard drawing is not needed; othervise return 0.

    • NTEX_AUTOPREVIEW - send if you enabled the item autopreview using the SetAutoPreviewHeight() function. You can cast the LXHDREX pointer to a LXHDREX_DIV pointer; the rcItem will contain the area in which you can draw the item preview.

    CtrlId

      the identifier of the list control sending this message / calling this callback

    iItem

      index of the current item (row)

    iSubItem

      index of the corrent subitem (column); note that this will actually be the id of the column and not the index of the column.

    dwItemData

      contain the item data for the current item

    dwMask

      currently unused; don't trust it

    dwFlag

      this variable can assume those values (some of them exclude others):

      • exBold - set this flag if you want a bold version of the standard font
      • exItalic - set this flag if you want an italic version of the standard font
      • exUnderline - set this flag if you want a underline version of the standard font. Those 3 flag (bold, italic and underline) cannot be combined.
      • exHFont - set this flag if you put a valid font handle in the hfText member
      • exTextColor - set this flag if the crText member is valid
      • exComboArray - this flag is set if the pComboArray is valid

    crText

      contain the color you wish for the text; the flag exTextColor has to be setted

    crBack

      actually unused

    hfText

      put here the HFONT handle for your custom font; the flag exHFont has to be setted

    csText

      contain the text of the item

    pComboArray

      if iNotifyCode is equal to NTEX_COMBOREQUEST, this will contain a valid pointer to a CStringArray object; you will fill this array with the items you want to appear in the combo box. For other iNotifyCode it is ignored.


    This is a sample for an exinfo callback function:

    long CSuperGrid1View::GetExInfoCallback(LXHDREX * pLx)
    {
    	ASSERT(pLx);
    
    
    	switch(pLx->iNotifyCode)
    	{
    	case NTEX_ITEMINFO:
    		// This will set a standard (default gui derived) bold fonts for first 10 items
    		if (iItem <10) pLx->dwFlag |= LXHDREX::exBold;
    
    		// This will set a standard (default gui derived) underline fonts for subitems of column id = 7
    		if (pLx->iSubItem == 7) pLx->dwFlag |= LXHDREX::exUnderline;
    
    		// This will set a custom font (ftTimes is a CFont object) for subitems of column id = 2
    		if (pLx->iSubItem == 2)
    		{
    			pLx->hfText = (HFONT) ftTimes.GetSafeHandle();
    			pLx->dwFlag |= LXHDREX::exHFont;
    		}
    		// This will set red color for subitems of column id = 3
    		if (pLx->iSubItem == 3) 
    		{
    			pLx->dwFlag |= LXHDREX::exTextColor;
    			pLx->crText = RGB(255,0,0);
    		}
    		// This will set violet color for subitems of column id = 8
    		if (pLx->iSubItem == 8) 
    		{
    			pLx->dwFlag |= LXHDREX::exTextColor;
    			pLx->crText = RGB(255,0,128);
    		}
    		return 1;
    		break;
    	case NTEX_COMBOREQUEST:
    		{
    		// This will fill the list for a combobox column (for the column with id = pLx->iSubItem)
    			pLx->pComboArray->Add(CString(""));
    		// we are adding the text that currently the subitem has to the list with this
    			pLx->pComboArray->Add(pLx->csText);
    			pLx->pComboArray->Add(CString("choice 1"));
    			pLx->pComboArray->Add(CString("choice 2"));
    			pLx->pComboArray->Add(CString("choice 3"));
    		// if we return 0, the combobox will not appear
    			return 1;
    		}
    		break;
    	case NTEX_SETTEXT:
    		// if we are taking the items from a database, for example, here is the place to update it
    		// this is called when user edits an item. The modified text is stored in pLx->csText
    		// if we returns 1 the list control will call SetItemText for the modified item, otherwise the item will be set to LPSTR_TEXTCALLBACK
    		return 1;
    	};
    
    	return 0L;
    }
    


  • b. The Text callback
    This function is called when the control needs text; basically it does the same things of the LVN_GETDISPINFO message. We need to define a function in the parent class (a view or a dialog) a function with this declaration:
    void GetTextCallback(int iIndex, int iSubItem, long lCode, CString &cs);
    and set it as the text callback in the list using:
    void SetGetTextCallback(void (CWnd::*fpGetTextCallback)(int , int, long, CString &));
    we still have to handle the LVN_GETDISPINFO message, even if we define the text callback; sometimes the control will call the callback and sometimes will send the message.
    If you want to use an approach like "set all items as callback when starting and setting the text with SetItemText when you show an item", useful to speedup the showing of items, do it in the OnGetdispinfoList1 and avoid to define a text callback. I suggest a code like this:
    
    void CMyView::GetTextCallback(int iIndex, int iSubItem, long lCode, CString &cs)
    {
    	cs = "";
    	// The iSubItem is the real iSubItem; to obtain the column id of this subitem you can call this function
    
    	int rc = wndList.GetColumnIndex(iSubItem);
    	
    	switch (rc)
    	{
    		case 1: cs = "1"; break;
    		case 2: cs = "2"; break;
    		case 3: cs = "3"; break;
    		default: cs.Format("%d, %d", lCode, rc); break;
    	}
    }
    
    void CMyView::OnGetdispinfoList1(NMHDR* pNMHDR, LRESULT* pResult) 
    {
    	LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
    
    	if(pDispInfo->item.mask & LVIF_TEXT)
    	{
    		long index = pDispInfo->item.iItem;
    		long subItem = pDispInfo->item.iSubItem;
    		long objCode = pDispInfo->item.lParam;
    		CString cs;
    		GetTextCallback(index, subItem, objCode, cs);
    		lstrcpyn(pDispInfo->item.pszText, cs, pDispInfo->item.cchTextMax);
    	}
    	if(pDispInfo->item.mask & LVIF_IMAGE) pDispInfo->item.iImage = 0;//set image to first in list	
    	*pResult = 0;
    }
    

    If we defined a column with the fhImage flag, the control will draw an image in its cells. The image will come from the imagelist we setted with the SetItemImageList() function. We can control the index of the image simply setting the string variable.
    In the sample function above, where 1,2 and 3 are Id of columns containing image subitams, we say to the cantrol to use image 1 for column id 1, image 2 for column id 2 and so on.



  • c. The category callback
    This function is needed for the categorized list view; it will be called for every item to assign them to the various categories. We need to define a function in the parent class (a view or a dialog) a function with this declaration:
    void CategoryCallBack(CGfxCategoryManager * pCatMan, long & lData);
    and set it as the text callback in the list using:
    wndList.SetCategoryCallback((void (CWnd::*)(CGfxCategoryManager * , long &))CategoryCallBack);
    This function will have in its parameters a pointer to the CGfxCategoryManager and the lParam of the current item. It will be call for every items to build up the categories. The categories this control supports are:
    • text categories
    • numeric range categories
    • date-time range categories

    Let's show a sample of a CategoryCallBack; this function will assign the items to numerical categories, the first from 0 to 10, the second category from 11 to 20 and so on. We'll use the items lParam as value for this check:

    void MyView::CategoryCallBack(CGfxCategoryManager * pCatMan, long & lData)
    {

      long value = lData;

      // Search for a category which contain the value
      int iCat = pCatMan->FindCategory(value);
      // If cat <0, that means that no category was found, so let's add one
      if (iCat <0)
      {

        CString cs;
        long val1 = value / 10;
        val1 *= 10;
        long val2 = val1 + 10;

        cs.Format("%d - %d", val1, val2);
        // We add a category with this function, givin as category name the range (ex. "10-20") and val1 & val2 as range min and max
        iCat = pCatMan->AddCategory(cs, val1, val2);

      }
      if (iCat >= 0)
      {

        // Now we can add the item to the category(es) whe finded / add
        pCatMan->AddItemToCategory(iCat, value);

      }

    }
    There are 3 version of the find routine; the first takes a char *, and search for a category with a particular name; the second takes a CTime, and search for a category containing the time (you can specify a time - range for catogory); the third takes a double and search for a category containing that value in its range.
    In this callback function, we search between numeric categories for a category containing value; if the controls find one, iCat will be > or equal to 0, and will be the index of a category. If we don't find a category for our item, we add one using the AddCategory function and providing a description for that category and a range (in this case a numerical range; it can be a time range or simply an internal name). After we get an index for a category, we use the AddItemToCategory function to add an item to a category. We can add an item to more than a category.

5. Dinamic database link

The control can be dinamically linked to a dao database; and subitems can be linked with other dao recordset. This permits to have combobox selection for subitems linked with other recordset, all automatically with one line of code.
Here's what is needed for dinamic database link:

  • a. Linking the database
    First you'll need to link the database with the control. You'll need to open the recordset you want to use (here, cDaoMain). A good place for doing linking is the view OnInitialUpdate or the dialog OnInitDialog:
    if (!(cDaoMain.IsOpen())) cDaoMain.Open();
    supposing we want to link a field with another recordset (here, cDaoSub) we open here the second recordset too:
    if (!(cDaoSub.IsOpen())) cDaoSub.Open();

    Now we can link the recordsets with the control and fill it with the items:
    // Turn the list refresh off - for speed
    wndList.SetRedraw(false);
    // Binds the control with the main database
    wndList.BindDaoRecordset(&cDaoMain);
    // Fill the columns with the fields info
    wndList.FillRecordsetColumn();
    // Links the sub recordset with a field
    // This function has 2 version, one with fields name and another with fields numeric position
    wndList.LinkRecordset("SubFieldName", &cDaoSub, "FieldCode", "FieldText");
    // Fill the list with the database items
    wndList.FillDaoItems();
    // Turn the refresh on
    wndList.SetRedraw(true);

    The subfield links work in this way. I suppose I have the main recordset with a field named "SubFieldName" which olds (for example) a long value. This long value is a code which refers to the second recordset, field "FieldCode". When the list control has to display a subitem of the field "SubFieldName", it searches through the second recordset for a "FieldCode" equals to the "SubFieldName", and when/if it finds it, it will display the contents of the "FieldText" field of the secondary code.
    When user choose to modify a subitem of the "SubFieldName" column, the control will show a combobox containing all items of second recordset and handle all automatically.

    For example, this is the Recordset1:

    Field 1

    SubFieldName

    Database item 1 1  
    Database item 2 2  


    And this is the Recordset 2:

    FieldCode

    FieldText

    1 This is subfield 1
    2 This is subfield 2



    When we'll link the first recordset to the list control, the column "SubFieldName" will contain "This is subfield 1" for the first item and "This is subfield 2" for the second item. When we will edit an item of the "SubFieldName" column, a combobox with the "This is subfield 1" and "This is subfield 2" will appear.

    We need to handle the LVN_GETDISPINFO message in the list parent and from here we'll call this function:
    wndList.OnDaoGetDispInfo(pNMHDR, pResult);
    Then we'll need to write the exinfo callback as told before and from it call:
    return wndList.GetExInfoCallback(pLx); Don't provide a callback function for the Text callback.

  • b. the category view with the database link
    The categorized view works with database too. The standard way, as told above, can still be used; but can be very slow for large database; so another way is provided (which is still slow with 20000 items database, but a bit faster).
    Another callback can be defined and assigned with this function:
    wndList.SetDaoCategoryCallback((void (CWnd::*)(CGfxCategoryManager *))CategoryDBaseCallBack); The CategoryDBaseCallBack should have this syntax:
    void MyView::CategoryDBaseCallBack(CGfxCategoryManager * pCatMan);
    The difference from this and the old one is that the normal category callback will be called for every list control items, while this lets you handle more deeply the categorize process. The function should be like this:

    void MyView::CategoryDBaseCallBack(CGfxCategoryManager * pCatMan)
    {
    	CString sql, cs;
    
    	BOOL bLoop;
    	int iCat;
    
    	sql.Format("MyField = 'value1'");
    	bLoop = cDaoSet.FindFirst(sql);
    	if (bLoop)
    	{
    		cs.Format();
    		iCat = pCatMan->AddCategory("Field value1", "Value1");
    	}
    	while (bLoop)
    	{
    		pCatMan->AddItemToCategory(iCat, cDaoSet.GetAbsolutePosition());
    		bFind = cDaoSet.FindNext(sql);
    	}
    
    	sql.Format("MyField = 'value2'");
    	bLoop = cDaoSet.FindFirst(sql);
    	if (bLoop)
    	{
    		cs.Format();
    		iCat = pCatMan->AddCategory("Field value2", "Value2");
    	}
    	while (bLoop)
    	{
    		pCatMan->AddItemToCategory(iCat, cDaoSet.GetAbsolutePosition());
    		bFind = cDaoSet.FindNext(sql);
    	}
    }
    

    This approach can be used if you are only interested in categorizing the records with "MyField" equals to "value1" and "value2" and not interested in others. This function will only be called once and not for every items, so it's up to you to call AddItemToCategory for every items you are interested into.


  • c. the sorting
    Sorting is handled internally using dao sorting/requery. It seems to be the fastest way (few milliseconds for 20000 items database ..).
    The sorting has not been tested in categorized view .. use at your own risk.

  • d. column customization
    Column and format customization is not available with dinamic linked database. You can however drag columns all around. Saving of column state is also unsupported and should not be used here.
    So don't call these function:
    • CreateColumnManager()
    • DefineColumn()
    • DefineDefaultColumns()
    • ReadFromProfile() & WriteToProfile()
    • SetItemImageList()
    • SetHeaderImageList()

  • e. helper functions
    There function are provided in the list control for easier database navigation (they can be used even when no database in connected):
    • bool EnableMoveNext();
    • bool EnableMovePrev();
    • bool EnableMoveLast();
    • bool EnableMoveFirst();
    • void OnMovePrevRecord();
    • void OnMoveNextRecord();
    • void OnMoveLastRecord();
    • void OnMoveFirstRecord();

  • f. adding and removing
    With dynamic link, removing a database item is possible, but since the control identify items with absolute database position, for now you'll need to refill the list when you delete or add a record. This maybe will be fixed in future.

6. Printing the control content

The control supports print through the parent view. Actually it doesn't support printing from a dialog, but this can be obtained easily using my classes in printing section. It will support it directly in future (if enough of you request it).
To print the list control content from a view, simply call these functions from the view:

void MyView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
	wndList.OnBeginPrinting(pDC, pInfo);
}

void MyView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo)
{
	wndList.OnEndPrinting(pDC, pInfo);
}

void MyView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
	wndList.OnPrint(pDC, pInfo);
}

You can handle the margins of the print with the SetMargins function (giving values in millimeters).
With the SetPrintFlag function you can handle this things:

  • ptUseColor - set this flag if you want to print the items with the custom colors you defined with the exinfo callback; otherwise the items will be printed in black
  • ptUseFonts - set this to use the font customization
  • ptListName - set this to print a single line text in the left bottom of the paper; the text can be set with the SetPrintName function
  • ptPageNumbers set this to print in center bottom of the paper the page number
  • ptPageDate - set this to print the date of printing in the right bottom of the paper

You need to set this flags in the OnBeginPrinting function; for example:

void MyView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
	wndList.SetPrintFlag(ptUseColor|ptUseFonts|ptListName|ptPageNumbers|ptPageDate);
	wndList.SetPrintName("here's the data name");
	wndList.SetMargins(15,10,10,10);
	wndList.OnBeginPrinting(pDC, pInfo);
}

7. The item Autopreview

At gentle request, I added the item Autopreview like Outlook98. With the SetAutoPreviewHeight(height) function you can set the height of the Autopreview pane below each item; if you set height to zero, the autopreview will be disabled.
When the preview is on, you'll recieve (with exinfo callback/message) a subcode of NTEX_AUTOPREVIEW will be sent.
What's the Autopreview ? (2K)

8. The (un)supported CGfxListView

I provide here a demo project with the list control implemented as a CListView derived class. I did it as a test and provide it here just for courtesy; I personally prefer to use a CView derived and manually create the list control in it. The (un)supported means that it's here, it works but you'll have to take it "as is" without expecting support from me.

9. What do I need to use the control ?

You simply need to include the "GfxListCtrl.h" file in the parent window header.
The classes you need to import from the demo projects are:

  • CGfxCategoryManager
  • CGfxColumnManager
  • CGfxCustomizeColumnDlg
  • CGfxFormatColumnDlg
  • CGfxHeaderCtrl
  • CGfxInCombo
  • CGfxInComboEdit
  • CGfxInComboList
  • CGfxInComboStatic
  • CGfxInEdit
  • CGfxListCtrl
  • CGfxListTip
  • CGfxPopupMenu

Two helper classes (LXHDREX and LXHDREX_DIV) are defined in the CGfxListCtrl files.
You'll also need to copy the toolbar resource "IDR_LISTMENUBMP" (needed for bitmapped header control menu). Two dialog template are also needed (IDD_GFX_CUSTOMIZECOL and IDD_GFX_FORMATCOL), but if you insert the classes using the Devstudio components, they will automatically be copied.
You'll also need to define 4 command id:

#define ID_GFX_FORMATHEADER             32801
#define ID_GFX_CUSTOMIZEHEAD            32802
#define ID_GFX_SORTASC                  32803
#define ID_GFX_SORTDESC                 32804

They should be automatically inserted in your app by Resource Editor when when you import the "IDR_LISTMENUBMP" toolbar resource.

10. Bonus class: the CGfxPopupMenu, a CMenu derived for bitmapped popup menu

The list control uses bitmapped menu for the header control customization, sorting and for the richedit controls in cell editing. You can use the class for bitmapped menu in your too.
The class is CGfxPopupMenu, and is mainly derived from my CSpawnMenu class (that you can find in Menu section - owner draw menu 4); the difference is this class in CMenu derived.
Typical usage of this class is here (in response to a right mouse click, for example):
CGfxPopupMenu cMenu;
cMenu.LoadMenu(IDR_MYMENU);
cMenu.LoadToolBarResource(A_TOOLBAR_WITH_BITMAP);
cMenu.RemapMenu(&cMenu);
cMenu.EnableMenuItems(&cMenu, this);
cMenu.TrackPopupMenu(TPM_LEFTALIGN|TPM_RIGHTBUTTON, spt.x, spt.y, this);
cMenu.DestroyMenu();
Refer to the CSpawnMenu article for more info about this class.
Note that this class doesn't support shortcut keys. If you need this feature, use the CSpawnMenu class instead.

11. Localizing the control

The control use internally some text strings which are stored as #define and not as string resource.
You can find those defines at the beginning of the GfxlistCtrl.cpp file and of the GfxFormatColumnDlg.cpp file.

12. Function reference guide

Here follows a reference to the main list control member functions:

void SetAutoPreviewHeight(int iHeight);
If iHeight is zero, the autopreview feature is disabled; otherwise, give the height you wish for autopreview. As long as we use listctrl as base class, we must have autopreview for all items and of fixed height. If will be ignored in categorized view.
int GetAutoPreviewHeight();
Returns the current autopreview height (if 0, the preview is disabled)
bool SetBkImage(UINT nIDResource);
Set an image as background.
bool SetBkImage(LPCTSTR lpszResourceName);
Set an image as background.
void SetBlendBitmap(const bool bBlend);
If we have an image as background, we can se the sected items to "blend" with the background. This will slow a lot the drawing; if someone has a good way to draw a "blend" image rect (actually I'm using the imagelist draw function with ILD_BLENDxx) please tell me!
void SetExInfoCallback(long (CWnd::*fpExInfoFun)(LXHDREX *));
Setup the callback function for for items extended info.
void SetCategoryCallback(void (CWnd::*fpCategoryFun)(CGfxCategoryManager * , long &));
Setup the callback function for categorized view.
void SetGetTextCallback(void (CWnd::*fpGetTextCallback)(int , int, long, CString &));
Setup the callback function for text retriving. You can use this to speed up the control, but you'll need to handle the LVN_GETDISPINFO message too. Don't set this callback function if you don't use the the LPSTR_TEXTCALLBACK way to store items. If you set the callback you won't be able to use the GetItemText() function to retrieve a subitem text.
void SetItemImageList(CImageList * pList);
Sets the image list used for items
void ModifyItemEx(const int iIndex, const int iSubItem, const long lData, CString &cs);
Modify a subitem text. You can use this function even in Categorized view mode; when the view is in normal list mode, the lData member is ignored. If in categorized view, the iIndex is ignored and you need the lData to identify the items.
void ModifyItemEx(const int iIndex, const int iSubItem, const long lData);
Same as above, but the subitem will be setted to LPSTR_TEXTCALLBACK.
void DeleteItemEx(const int iIndex, const long lData);
Deletes an item. If in categorized view, you'll need to supply the lData and the iIndex will be ignored.
void GetItemTextEx(const int idx, const int sub, CString &cs, const unsigned long *dwData = NULL);
Get a subitem text; if you setted the callback function, it will be used for retriving the text. If in categorized view, you'll need to supply the lData member to identify the item, and the idx will be ignored.
int InsertItemEx(const int iIndex, const long lData);
Insert an item at position iIndex and with lParam lData. Works even in categorized view, but iIndex will be ignored. The item will be inserted in LPSTR_TEXTCALLBACK mode.
void EnableCategoryView(const bool bEnab = true);
Enables and disables the categorized view.
CGfxCategoryManager * GetCategoryManager();
Get the category manager. If it returns null, it means that the category mode is not actually enabled.
void RetrieveColumnData();
Syncronize the header control with the column datas; you need to call before saving the column state.
void SetupColumnData();
Fill the header controls with the columns; to be used at startup after we defined the standard colomuns.
CGfxColumnManager * CreateColumnManager();
Create the column manager; usually called in initialization (OnInitialUpdate or OnInitDialog) before adding the columns.
CGfxColumnManager * GetColumnManager();
Get a pointer to the column manager.
void EnableColumnDrag(const bool bEnableDrag = true);
Enable / disable the column dragging; by default it is enabled.
bool IsColumnDragEnabled() const;
Returns true is column drag is enabled.
int GetColumnIndex(const int iCol) const;
Return the column ID gived the column index (absolute pos).
bool DoCopyInClipboard(const bool bOnlySel = true);
Copy list items in the clipboard; if bOnlySel is true only selected items will be copied. Othervise all the items.
int GetColumnCount() const;
Return the number of columns.
void SetHeaderImageList(CImageList * pIma);
Sets the imagelist for the header.
void SetSubItemImage(const int iItem, const int iSubItem, const int iIndex);
It's the same of SetItemText(), but it takes an index of the item image list instead. You need to define the fhImage flag for the column to have the image displayed.
void RemoveAllSelection();
Remove the selection from the item(s). Doesn't remove the item(s), only the selection.
void GetSubItemRect(const int nItem, const int nCol, CRect &rcItem);
Get the bounding rect for a subitem.
bool EnsureSubItemVisible(const int iSubItem);
Ensure a sub item visibility.
void OnInit();
To be called if the list control is "auto" created (like in dialog and formview); you will not need to call this if you explicity create the list control (if you called the .Create(..)).
void ModifyFlag(const DWORD dwRemove, const DWORD dwAdd, const UINT dwRedraw);
Modify the flags of the list control.
DWORD GetFlag();
Get the flags variable.
int InsertColumnEx(int iPos, const char * cText, int iAlign, int iWidth, DWORD exData);
Insert a column directly in the list control. Exdata contain the flags for the column (fhComboBox, fhEdit ..) while iPos is the Id of the column.
void OnGfxCustomizehead();
Shows the column customize dialog box.
void OnGfxFormatheader();
Shows the column format dialog box.
void ModifyExStyle(DWORD dwRemove, DWORD dwAdd, UINT dwRedraw = 0);
Permits to set the new extended style of the list controls (those from new commctrl32.dll).

Printing support functions:
void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
The control supports (for now, badly ..) printing process. You have to call this function from the view OnBeginPrinting() if you want to print the list control.
void OnPrint(CDC* pDC, CPrintInfo* pInfo);
The control supports (for now, badly ..) printing process. You have to call this function from the view OnPrint() if you want to print the list control.
void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
The control supports (for now, badly ..) printing process. You have to call this function from the view OnEndPrinting() if you want to print the list control.
void SetPrintFlag(const DWORD dwf);
Setup the printing flags (use of colors, fonts ..); see above for information.
void SetPrintName(const char * name);
Set the name of the control for printing if you setted the ptListName flag.
void SetMargins(const int left, const int top, const int right, const int bottom);
Set the margins for the page; used in printing. The value are in millimeters.

Dynamic Dao link functions:
bool LinkRecordset(const char * cMainRecord, CDaoRecordset * ps, const char * cLinkRecord, const char * cTextField);
Call this to link a field of the database with the field of another recordset and select the field of the second recordset you want to be displayed. All with names of recordset's fields.
bool LinkRecordset(const int iColId, CDaoRecordset * ps, const int iLnk, const int iTxtLnk);
Call this to link a field of the database with the field of another recordset and select the field of the second recordset you want to be displayed. This version use absolute indexes of fields.
bool EnableMoveNext();
Returns true if it is possible to move the selection to the next item.
bool EnableMovePrev();
Returns true if it is possible to move the selection to the previous item.
bool EnableMoveLast();
Returns true if it is possible to move the selection to the last item.
bool EnableMoveFirst();
Returns true if it is possible to move the selection to the first item.
void OnMovePrevRecord();
Move selection to the previous item / record.
void OnMoveNextRecord();
Move selection to the next item / record.
void OnMoveLastRecord();
Move selection to the lst item / record.
void OnMoveFirstRecord();
Move selection to the first item / record.
void SetDaoCategoryCallback(void (CWnd::*fpDaoCategoryFun)(CGfxCategoryManager *));
Setup the callback function for for dao category callback (see documentation above for info).
bool BindDaoRecordset(CDaoRecordset * pSet);
Binds the control to a recordset.
bool FillRecordsetColumn();
Fill the control columns from the recordset.
bool FillDaoItems();
Fill the control with items from the recordset.
void OnDaoGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult);
To be called from the OnDispInfo in the parent window.
void OnDaoGetTextCallback(int iIndex, int iSubItem, long lCode, CString &cs);
Used internally.
long GetExInfoCallback(LXHDREX * pLx);
To be called from the exinfo callback of the parent window.

That's all folks! Hope you'll enjoy this thing. Thanks to my beta-tester Harald (and to Austria soccer/wrestling team).
I'd like to hear if you like this control; I'll be happy to hear of bugs (and especially if you find fix).
I'm offering no support for what concerns database. Dynamic database is here only because I did it as a test. I understand little of dao database and ABSOLUTELY nothing of ODBC database and I've not time or interest in ODBC. So if you want support for it you'll have to write by yourself and please don't ask me (you will not like the answer).

Last updated: 21 June 1998



Comments

  • XP-Style

    Posted by Legacy on 09/24/2003 12:00am

    Originally posted by: Horst Richter

    I'm using the GfxListCtrl for a long time. No i decided to give my application the Windows XP style. Since this i've several problems with the header control. the grid is missing also the text will be drawn wrong if you select a column.
    Has anybody the same problems and has a solution for this problem?

    Reply
  • Virtual List Control

    Posted by Legacy on 09/19/2003 12:00am

    Originally posted by: Frode Gorseth

    Hello.
    Has anybody used this excellent GfxListCtrl as a Virtual List Control to handle a large number of items (list view with LVS_OWNERDATA using LVN_GETDISPINFO)?

    Best regards,
    Frode Gorseth.

    • good control

      Posted by victording on 07/27/2007 11:03am

      it is the control i want to have.

      Reply
    Reply
  • Excellent =)

    Posted by Legacy on 11/11/2002 12:00am

    Originally posted by: Jason Mingl

    Thanks to the author, wherever you are. This is a great little piece of code.

    It needed (still needs, actually) some tweaking and I had to extend it to support background colors in rows, automatic expanding of categories etc, but it saved me some time and taught me a great deal about working with these controls.

    Reply
  • Problem with Tips

    Posted by Legacy on 12/24/2001 12:00am

    Originally posted by: Sunday

    I created an application with a grid into my view and another grid into a dialog.
    When dialog is open, and mouse cross over the grid, a tip must be displayed, but application switch to the grid in my view instead to stay into the grid in my dialog.

    Can you help me to solve this problem?

    Bye, SunDay.

    Reply
  • Spin button support???

    Posted by Legacy on 09/24/2001 12:00am

    Originally posted by: Alexis Harper

    Does anyone know how to add spin button control functionality to the CGfxListCtrl?

    Reply
  • help please`

    Posted by Legacy on 09/18/2001 12:00am

    Originally posted by: ZHB

    I used the gfxlistctrl in my program and now I meet a problem I can't slove it,I create two kinds of  GetCategory
    
    and use the radio button to exchange it,my sourcecode is below:
    void CShowBill::OnRadio1()
    {
    // TODO: Add your control notification handler code here
    if (m_lstCtrl.GetCategoryManager())
    m_lstCtrl.EnableCategoryView(false);
    //the m_IsRoomType is decide to change GetCategory
    this->m_IsRoomType = false;
    m_lstCtrl.EnableCategoryView(true);
    }

    void CShowBill::OnRadio2()
    {
    // TODO: Add your control notification handler code here
    if (m_lstCtrl.GetCategoryManager())
    m_lstCtrl.EnableCategoryView(false);
    this->m_IsRoomType = true;
    //those is the class
    m_iCatcab =-1;
    m_iCatboard =-1;
    m_iCatfitting =-1;
    m_iCatbaishe = -1;
    m_lstCtrl.EnableCategoryView(true);
    }
    when I set the breakpoint in those functions and trace the program ,It will be well,but if I cancle the breakpoint to run it, it will display a error about memory.
    please help me

    Reply
  • Help please

    Posted by Legacy on 03/28/2001 12:00am

    Originally posted by: BLaZe

    Can someone help me with this library, When I make a project, after I include all the CGfx files in my project, I don't begin to code it and I have 233 errors, someone can help me plz ?

    And also How can make a DAO DataBase ?

    TIA

    please reply to mpblaze@iquebec.com
    Thanks

    BLaZe

    Reply
  • Great List but missing Printing from Dialog, Please support it

    Posted by Legacy on 01/21/2001 12:00am

    Originally posted by: Osama Ahmad

    I found this as a very good list but I need to use it and print from it in a dialogbox.

    Reply
  • Performance issue when list contains more than 60,000 rows

    Posted by Legacy on 11/06/2000 12:00am

    Originally posted by: Vish Khasarla

    I have case where the list ( CGfxList ) consists of more than 60,000 rows with 10 columns and the column sort is enabled.

    Given this, if I were to sort on the column or just just use Ctrl+End or "Page Up" or "Page Down" buttons, the list responds very slowly.

    Is there anyway to improve the ( page up or page down )performance of the list?

    -Vish

    Reply
  • RecordView using a second recordset with RecordsetView

    Posted by Legacy on 04/19/2000 12:00am

    Originally posted by: Liz

    It is excellent. I have one question. How would you have handle it you were using CRecordview. Not using OnInitialUpdate function. If the recordview was a second form?
    

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds