Click here to Skip to main content
15,867,834 members
Articles / Programming Languages / C

Win32 SDK Data Grid View Made Easy

Rate me:
Please Sign up or sign in to vote.
4.88/5 (37 votes)
26 May 2010CPOL13 min read 118.9K   7.5K   128   23
This article describes the creation of a non-MFC custom control
Demo screenshot

Contents

Introduction

This is the third in a series of articles concerning Windows SDK development that I have written over the years. The first two are:

  1. Win32 SDK C Tab Control Made Easy [^]
  2. Win32 SDK C Autocomplete Combobox Made Easy [^]

Someone once asked me why I would want to code a control in C when I could utilize the object oriented power of C++. Indeed, the data grid view presented here is nothing new and there are quite a few projects out there, most of them MFC based, that are quite good. The answer to that question has more to do with my coding interests and personal, professional development than anything else.

I primarily develop automated test applications used in engineering and manufacturing. I started out coding engineering applications in VB.NET using vendor authored frameworks that wrapped pre-existing COM based frameworks that in turn wrapped C based IO communications frameworks. I was able to quickly throw together an application that worked but that was somewhat slow and bloated. In addition to this, I wanted to do things from the outset that the vendor supplied frameworks didn't make provision for, and many examples that I found were written in C and utilized the C based IO layer directly.

In my quest to learn enough C to adapt the code examples to my needs, I discovered BCX - The Basic to C Translator [^] a tool that allowed me to take understandable snippets of basic and convert them to C so that I could, for the first time, see beneath the abstraction layer of the higher language. This method of self education was particularly effective because my level of interest and concentration are peaked when trying to solve a particular problem -- something that does not necessarily happen when reading a textbook or technical publication.

In order to cement my understanding of C, I wrote some windows based applications using the Pelles C [^] compiler and IDE. At this point, I began to understand how Windows worked; especially messaging, callbacks, pointers, and function pointers. This in turn helped me understand how VB.NET and C# worked under the hood.

That, in a nutshell, is my journey. I now primarily code in C#, I like the syntax, and most code examples (VB.NET, JAVA, C++, MFC) can be adapted to my projects with little effort. That said, some of the more interesting challenges and lessons along the way come out of the C based projects I've written.

The Grid -- A Custom Control

The Windows SDK provides a small selection of basic tools that handle the majority of user interface tasks but there is often a need for something extra. The fact that there are many examples out there of grid type controls attests to this. It would have been nice if Microsoft had included a grid in its standard collection of SDK widgets but in all fairness there is no implementation of an editable grid anywhere in the operating system. I wanted a message based custom grid control but I didn't want to write and draw the entire thing myself so I looked around at some of the MFC based grid projects out there and got some ideas for this custom control.

Using the Grid

This is the easy part. First include the custom control's header file in the project:

C++
#include "DataGridView.h"

This Data Grid View is a message based, custom control and as such must be initialized before use. In the WinMain() method of the demo, call the control's initializer just after the call to InitCommonControlsEx().

C++
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
	LPSTR lpszCmdLine, int nCmdShow)
{
    INITCOMMONCONTROLSEX icc;
    WNDCLASSEX wcx;

    ghInstance = hInstance;

    /* Initialize common controls. Also needed for MANIFEST's */

    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_WIN95_CLASSES /*|ICC_COOL_CLASSES|
	ICC_DATE_CLASSES|ICC_PAGESCROLLER_CLASS|ICC_USEREX_CLASSES*/;
    InitCommonControlsEx(&icc);

    InitDataGridView(hInstance);

In the resource dialog Editor, place a custom control holder and give it the Windows class name for this control.

Custom control

Messages and Macros

Configure the control to do what you want using Windows messages. To make this easy and as a way of documenting the messages, I created macros for each message. If you prefer to call SendMessage() or PostMessage() explicitly, please refer to the macro defs in the header for usage.

C++
COLORREF DataGridView_GetEditorBkColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the editor's back color.
C++
BOOL DataGridView_SetEditorBkColor(
     HWND hwnd
     COLORREF clrBk
     );
Parameters
hwnd
     Handle to the data grid view control.
clrBk
     Background color to set.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
COLORREF DataGridView_GetEditorTxtColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the editor's text color.
C++
BOOL DataGridView_SetEditorTxtColor(
     HWND hwnd
     COLORREF clrTxt
     );
Parameters
hwnd
     Handle to the data grid view control.
clrTxt
     Editor's new text color.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
HWND DataGridView_GetListViewControl(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the handle to the list view control if successful or NULL otherwise.
C++
HWND DataGridView_GetEditControl(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the handle to the edit control if successful or NULL otherwise.
C++
int DataGridView_GetColumnCount(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the number of columns.
C++
int DataGridView_GetRowCount(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the number of rows.
C++
BOOL DataGridView_SetResizableHeader(
     HWND hwnd
     BOOL fResizable
     );
Parameters
hwnd
     Handle to the data grid view control.
fResizable
     Default TRUE columns may be resized.  Otherwise column widths frozen.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
COLORREF DataGridView_GetBkColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the data grid view's back color.
C++
BOOL DataGridView_SetBkColor(
     HWND hwnd
     COLORREF clrBk
     );
Parameters
hwnd
     Handle to the data grid view control.
clrBk
     Background color to set or the CLR_NONE value for no background color.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
COLORREF DataGridView_GetTextColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the data grid view's text color.
C++
BOOL DataGridView_SetTextColor(
     HWND hwnd
     COLORREF clrTxt
     );
Parameters
hwnd
     Handle to the data grid view control.
clrTxt
     Data grid view's new text color.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
COLORREF DataGridView_GetAltBkColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the data grid view's alternate back color.
C++
BOOL DataGridView_SetAltBkColor(
     HWND hwnd
     COLORREF clrBk
     BOOLfPaintByRow
     );
Parameters
hwnd
     Handle to the data grid view control.
clrBk
     Alternate background color to set.
fPaintByRow
     TRUE to paint alternate rows or FALSE to paint alternate columns.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
COLORREF DataGridView_GetAltTextColor(
     HWND hwnd
     );
Parameters
hwnd
     Handle to the data grid view control.

Return Values
Returns the data grid view's alternate text color.
C++
BOOL DataGridView_SetAltTextColor(
     HWND hwnd
     COLORREF clrTxt
     );
Parameters
hwnd
     Handle to the data grid view control.
clrTxt
     Data grid view's new alternate text color.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
BOOL DataGridView_DisplayRowHeaders(
     HWND hwnd
     BOOL fShow
     );
Parameters
hwnd
     Handle to the data grid view control.
fShow
     TRUE to draw row header buttons in first column.
     Default FALSE to display normal first column.

Note: if this is set to TRUE column header and row header will
     indicate selected cell by depressed button state.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
BOOL DataGridView_SetRowHeight(
     HWND hwnd
     int iHeight
     );
Parameters
hwnd
     Handle to the data grid view control.
iHeight
     Specifies the height, in pixels, of the rows.
Return Values
Returns TRUE if successful or FALSE otherwise.
C++
BOOL DataGridView_ExtendLastColumn(
     HWND hwnd
     BOOL fExtend
     );
Parameters
hwnd
     Handle to the data grid view control.
fExtend
     TRUE to extend last column to the edge of the control otherwise false.
Return Values
Returns TRUE if successful or FALSE otherwise.

In addition to the above macros, I have defined three that simplify setting up and loading data into the grid.

C++
void DataGridView_AddColumn(
     HWND hGrid
     int nCol
     int iWidth
     LPSTR szColText
     );
Parameters
hGrid
     Handle to the data grid view control.
nCol
     The number of this column.
iWidth
     The width of this column.
szColText
     The column header text.
Return Values
Returns nothing.
C++
void DataGridView_AddColumns(
     HWND hGrid
     LPSTR* aryColTxt
     int iColCount
     );
Parameters
hGrid
     Handle to the data grid view control.
aryColTxt
     An array of column header text strings.
iColCount
     The number of columns desired (length of aryColTxt).
Return Values
Returns nothing.
C++
void DataGridView_AddRow(
     HWND hGrid
     LPSTR* aryItemTxt
     int iColCount
     );
Parameters
hGrid
     Handle to the data grid view control.
aryItemTxt
     An array of item and subitem strings.
iColCount
     The number of columns desired (length of aryItemTxt).
Return Values
Returns nothing.

Creating a Custom Control

Writing a custom control begins with defining its windows class (this is not to be confused with a C++ class.) A look at the InitDataGridView() function in DataGridView.c demonstrates how this is done.

C++
BOOL InitDataGridView(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;
    ATOM aReturn;

    wcex.cbSize          = sizeof(WNDCLASSEX);
    wcex.style           = CS_BYTEALIGNCLIENT;
    wcex.lpfnWndProc     = (WNDPROC)Grid_WndProc;
    wcex.cbClsExtra      = 0;
    wcex.cbWndExtra      = 0;
    wcex.hInstance       = hInstance;
    wcex.hCursor         = NULL;
    wcex.hbrBackground   = (HBRUSH)(GetStockObject(GRAY_BRUSH));//(COLOR_WINDOW + 1);
    wcex.lpszMenuName    = NULL;
    wcex.lpszClassName   = g_szClassName;
    wcex.hIcon           = NULL;
    wcex.hIconSm         = NULL;

    aReturn = RegisterClassEx(&wcex);

This information is used by Windows when CreateWindow() or CreateWindowEx() is called to create an instance of the control. In the demo, Windows creates the control from the resource script internally probably by calling CreateWindowEx().

The public interface of a message based control is presented in the form of windows messages defined in the control's header file. Messages are passed to a control primarily by means of the SendMessage() Windows API function. SendMessage() returns the result codes returned by the control's callback procedure and in this way achieves the functionality of property getters and setters common to Object Oriented languages today.

When I set about to define messages for this control, I used the macro defs for the list view found in commctrl.h as a pattern and an example. My intent was to write the messages to mimic as closely as possible those of a standard Microsoft control. I noticed that certain values (BOOLs and INTs) are typically packed into the wParam and others (struct pointers and strings) are typically packed into the lParam. There are historical reasons for this [^] , however both parameters now are of the same type.

Here I define a message and its corresponding parameters in a macro:

C++
#define DGVM_SETEDITORTEXTCLR WM_USER + 0x07
#define DataGridView_SetEditorTxtColor(hwnd,clrTxt)  \
     (BOOL)SNDMSG((hwnd),DGVM_SETEDITORTEXTCLR,0,(LPARAM)(COLORREF)(clrTxt))

Managing Messages

The messages are handled within the control in its main callback procedure Grid_WndProc() a portion of which you can see here.

C++
static LRESULT CALLBACK Grid_WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
     // Update g_lpInst for this message
     if (Grid_GetInstanceData(hwnd, &g_lpInst))
     {
          if (LVM_SETITEMA == msg || LVM_SETITEMW == msg)
          {
               if (0 == ((LPLVITEM)lParam)->iItem)  // move Editor to item 0 subitem 0
                    CallWindowProc((WNDPROC)ListView_Proc, g_lpInst->hwndList,
                         WM_KEYDOWN, VK_LEFT, 0L);
          }
          // Pass along any list view specific messages transparently
          if (LVM_FIRST <= msg && msg <= LVM_FIRST + 181)
               return SNDMSG(g_lpInst->hwndList, msg, wParam, lParam);
     }
     switch (msg)
     {
               HANDLE_MSG(hwnd, WM_CTLCOLOREDIT, Grid_OnCtlColorEdit);
               HANDLE_MSG(hwnd, WM_CREATE, Grid_OnCreate);
               HANDLE_MSG(hwnd, WM_DESTROY, Grid_OnDestroy);
               HANDLE_MSG(hwnd, WM_GETDLGCODE, Grid_OnGetDlgCode);
               HANDLE_MSG(hwnd, WM_SIZE, Grid_OnSize);
               HANDLE_MSG(hwnd, WM_NOTIFY, Grid_OnNotify);
               HANDLE_MSG(hwnd, WM_SETCURSOR, Grid_OnSetCursor);
               HANDLE_MSG(hwnd, WM_SETFOCUS, Grid_OnSetFocus);

          case WM_NOTIFYFORMAT:
#ifdef UNICODE
               return NFR_UNICODE;
#else
               return NFR_ANSI;
#endif

          case DGVM_GETLISTVIEWCONTROL:
               return (LRESULT)g_lpInst->hwndList;

          case DGVM_GETEDITCONTROL:
               return (LRESULT)g_lpInst->hwndEditor;

         //
         // Skip cases for brevity
         //

          default: return DefWindowProc (hwnd, msg, wParam, lParam);
     }
}

There are several things that I want to point out here.

First, all of the private fields that I want to persist with this instance of the control, I maintain in a structure linked to this instance. Since all processes within the code module originate here, I update the global pointer using Grid_GetInstanceData(hwnd,&g_lpInst) to refer to the struct for this instance. This is analogous to the self referencing this pointer of C++ or me pointer in VB.

Second, it is very easy for this particular procedure to get out of hand. I have encountered callbacks that took up nearly 50% of a code module and became, in effect spaghetti with messages being sent or posted from one case branch to another. They can be quite difficult to debug when this happens so there are some rules of thumb that I use in my code to keep things neat and tidy.

  1. Use message crackers for all standard messages -- These are found primarily in windowsx.h and commctrl.h and can be implemented easily using a wizard [^]. Message crackers part out messages to various sub procedures where they can be handled discretely.
  2. Custom messages may be handled in the callback procedure if it can be done in only a few lines (all should be visible in the monitor) and shouldn't require the declaration of a variable. To put it another way, these messages should be treated the same way one would a property getter or setter in an OO language. When in doubt, part it out.

Third, due to some helpful feedback from CodeProject and Pelles C communities I have included some improvements, one of which is Unicode support. Notice that the grid now handles the WM_NOTIFYFORMAT message. WM_NOTIFY messages include a notification message code that can either target wide character or ANSI format. ANSI is the default even if the control was created using CreateWindowExW() and therefore I must explicitly specify the desired format of the code here. (More on Unicode later.)

The Control's Private Fields

Here is the struct that contains any data that I want to persist with an instance of the grid control.
C++
typedef struct _tagINSTANCEDATA{
     HINSTANCE hInstance;
     HWND hwndList;
     HWND hwndEditor;
     LVHITTESTINFO hti;
     COLORREF Editor_TxtColr;
     COLORREF Editor_BkColr;
     COLORREF Cell_AltTxtColr;
     COLORREF Cell_AltBkColr;
     BOOL fPaintByRow;
     BOOL fRowHeaders;
     BOOL fResizableHeader;
     BOOL fExtendLastCol;
     BOOL fSuppressEraseBackground;
     BOOL fsizeCol;
}INSTANCEDATA, *LPINSTANCEDATA;
I created some helper methods to allocate and attach, retrieve, and destroy this structure during the control's life cycle. In this way, there are practically no limitations on how many instances of the control there can be at any given time.

C++
static BOOL Grid_CreateInstanceData(HWND hGrid, LPINSTANCEDATA pInstanceData)
{
     LPINSTANCEDATA pInst = (LPINSTANCEDATA)malloc(sizeof(INSTANCEDATA));
     memmove(pInst, pInstanceData, sizeof(INSTANCEDATA));

     return SetProp(hGrid, TEXT("lpInsData"), pInst);
}

static BOOL Grid_GetInstanceData(HWND hGrid, LPINSTANCEDATA * ppInstanceData)
{
     *ppInstanceData = (LPINSTANCEDATA)GetProp(hGrid, TEXT("lpInsData"));
     if (NULL != *ppInstanceData)
          return TRUE;
     return FALSE;
}

static BOOL Grid_FreeInstanceData(HWND hGrid)
{
     LPINSTANCEDATA pInst;
     if (Grid_GetInstanceData(hGrid, &pInst))
     {
          free((LPINSTANCEDATA)pInst);
          RemoveProp(hGrid, TEXT("lpInsData"));
          return TRUE;
     }
     return FALSE;
}

The Control's Constructor (sort of)

Since Windows classes are message based, naturally there is a message fired to indicate that the control is being built. WM_CREATE is one of the first messages that our control receives as it comes into existence. The message cracker handler for WM_CREATE serves as a constructor for the control.

C++
static BOOL Grid_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
     INSTANCEDATA inst;

     // initialize hit test info and scroll info
     memset(&inst.hti, -1, sizeof(LVHITTESTINFO));

     // get the hInstance
     inst.hInstance = lpCreateStruct->hInstance;

    // create the ListView control
    inst.hwndList = CreateListView(lpCreateStruct->hInstance, hwnd);
     if(NULL == inst.hwndList) return FALSE;

     // gridlines default
     ListView_SetExtendedListViewStyle(inst.hwndList,LVS_EX_GRIDLINES);

     // default ListView Colors
     inst.fPaintByRow = TRUE;
     inst.Cell_AltTxtColr = ListView_GetTextColor(inst.hwndList);
     inst.Cell_AltBkColr = ListView_GetBkColor(inst.hwndList); //White

     // default ListView pseudoHeaders off
     inst.fRowHeaders = FALSE;

     inst.fExtendLastCol = FALSE;

    inst.hwndEditor = CreateCellEditor(lpCreateStruct->hInstance, hwnd);
     if(NULL == inst.hwndEditor) return FALSE;

     // default Cell Editor Colors
     inst.Editor_BkColr = ListView_GetBkColor(inst.hwndList);
     inst.Editor_TxtColr = ListView_GetTextColor(inst.hwndList);

     Grid_SetRowHeight(inst.hwndList, 20);

     return Grid_CreateInstanceData(hwnd,&inst);
}

In the above code, I create a list view and an edit control as children of the Data Grid View. The Data Grid View custom control class wraps these two controls and any notifications that they post are handled internally by the Data Grid View control. Now let's have a look at CreateCellEditor().

C++
static HWND CreateCellEditor(HINSTANCE hInstance, HWND hwndParent)
{
     DWORD dwStyle, dwExStyle;
     HWND hwnd;

     dwStyle = WS_CHILD | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_WANTRETURN;

     dwExStyle = WS_EX_TRANSPARENT | WS_EX_LEFT;

     hwnd = CreateWindowEx(
          dwExStyle,      // ex style
          WC_EDIT,        // class name - defined in commctrl.h
          TEXT(""),       // dummy text
          dwStyle,        // style
          0,              // x position
          0               // y position
          0,              // width
          0,              // height
          hwndParent,     // parent
          (HMENU)ID_EDIT, // ID
          hInstance,      // instance
          NULL);          // no extra data

     if (!hwnd)
          return NULL;

     SendMessage(hwnd, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), 0L);

     // Subclass Editor and save the OldProc
     SetProp(hwnd, TEXT("Wprc"), (HANDLE)GetWindowLongPtr(hwnd, GWL_WNDPROC));
     SubclassWindow(hwnd, Editor_Proc);

     return hwnd;
}

This is boiler plate instantiation with the exception of the last few lines where I subclass the newly created edit control. This will allow me to override default behavior in a windows message based control. Specifically I provide Editor_Proc() as the new target for any messages to the edit component. Let's have a look at Editor_Proc().

C++
static LRESULT CALLBACK Editor_Proc (HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
{
     static TCHAR buf[2048];

     if(WM_DESTROY==msg) // UnSubclass the Edit Control
     {
          SetWindowLong(hEdit,GWL_WNDPROC,(DWORD)GetProp(hEdit,TEXT("Wprc")));
          RemoveProp(hEdit,"Wprc");
          return 0;
     }
     else if(WM_CHAR==msg&&VK_RETURN==wParam)
          return TRUE; // handle Enter (NO BELL)

     //
     // Skip some stuff for brevity
     //

     else if(WM_KEYDOWN==msg&&VK_ESCAPE==wParam) // handle Escape (NO WM_DESTROY)
     {
          Grid_GetInstanceData(GetParent(hEdit),&g_lpInst);
          //Get the SubItem Text
          ListView_GetItemText(g_lpInst->hwndList,
               g_lpInst->hti.iItem,g_lpInst->hti.iSubItem,buf,sizeof buf);
          //Reset the editor
          Edit_SetText(hEdit, buf);
          // Redraw Editor so it doesn't disappear
          Edit_CenterTextVertically(hEdit);

          //Focus back to listview
          SetFocus(g_lpInst->hwndList);
          return 0;
     }
      return CallWindowProc((WNDPROC)GetProp(hEdit,
		(LPCSTR)TEXT("Wprc")),hEdit,msg,wParam,lParam);
}

This proc differs from the top level Grid_WndProc() in several particulars. First notice how WM_DESTROY is handled. This is boiler plate code that ensures that before the Grid is destroyed, the allocated property storage for the edit control's base callback procedure's pointer is freed. Second, notice the last line where we route un-handled messages, not to DefWindowProc(), but to the edit control's base window procedure. These two pieces of boilerplate code serve as a template for any subclassed component's callback procedure and anything we want to override goes between them.

Up to this point, I have described the backbone of the Data Grid View's design, much of which provides for basic message routing. This type of architecture could be applied to any custom control that includes, within itself, subcomponent controls. There were several technical challenges to overcome to get the whole thing to work together in a way that felt natural and was visually appealing. The remainder of this article will focus on those particulars.

Points of Interest

The list view provides for resizable columns but resizing rows is not quite so straight forward. I employed the following trick to accomplish this.

C++
VOID Grid_SetRowHeight(HWND hList, INT iRowHeight)
{
	static HIMAGELIST hIconList;
	ImageList_Destroy (hIconList);
	hIconList = ImageList_Create(1,iRowHeight,ILC_COLOR,0,1);
	ListView_SetImageList(hList,hIconList,LVSIL_SMALL);
}

The rows of a list view will resize to accommodate whatever type of image is to be displayed even if there are no images in the list.

The Edit box needed to be nearly invisible. By this I mean that when it was positioned over a cell, the text should not appear to move about. In order to achieve this effect, it was necessary to center the edit™s text vertically. There were a few solutions out there but they tended to be rather elaborate. I distilled it all down to the following simple method.

C++
VOID Edit_CenterTextVertically(HWND hwnd)
{
     RECT rcTxt = {0,0,0,0};
     RECT rcEdt = {0,0,0,0};
     HDC hdc;

     //calculate client area height needed for a font
    hdc = GetDC(hwnd);
    DrawText(hdc, TEXT("Ky"), 2, &rcTxt, DT_CALCRECT | DT_LEFT);
    ReleaseDC(hwnd, hdc);

     // Set top and left margins
     GetClientRect(hwnd,&rcEdt);
    rcEdt.left += 4;
     rcEdt.top = ((rcEdt.bottom - (rcTxt.bottom - rcTxt.top)) / 2);

     Edit_SetRect(hwnd, &rcEdt);
}

One of the problems with making a data grid view from an edit and a list view is ensuring that the edit box sizes appropriately if the column width is changed in a mouse drag operation. The data grid view projects, that I looked at, didn't handle this well. Typically the edit would become under or over sized until moved via mouse click or key board navigation. I spent some time observing the list view and the messages it issued during the column resize and finally hit upon a nifty trick. The list view's header will issue an HDN_ITEMCHANGING notification to signal that one of its items is beginning to change size. I only needed a cue that the sizing operation had concluded and then I noticed the following behavior.

Demo screenshot

Notice that the cursor changes once the mouse button is released and the mouse position slips off of the header. This is the perfect indication that a sizing operation has completed. In fact, if the mouse button is down the cursor will not change no matter where in the list view it is positioned (sizing is still taking place). In addition to this, if sizing has begun and the mouse button is momentarily released over the header sizing mode does not change until the cursor slips off of the header.

After having made these observations, I put together the following code to handle resizing.

C++
BOOL Grid_OnSetCursor(HWND hwnd, HWND hwndCursor, UINT codeHitTest, UINT msg)
{
     //
     // In Grid_OnColumnResize() we set g_fsizeCol 
     // when we clicked on the listview header;
     //  now we have a means of knowing that the uncaptured cursor has
     //  just slipped off the header.
     //
     if(g_lpInst->fsizeCol)
     {
          RECT rc;

          //Reposition the editor
          ListView_GetSubItemRect(g_lpInst->hwndList,g_lpInst->
		hti.iItem,g_lpInst->hti.iSubItem,LVIR_LABEL,&rc);
          MapWindowPoints(g_lpInst->hwndList, hwnd, (LPPOINT)&rc.left,2);
          MoveWindow(g_lpInst->hwndEditor,
               rc.left,rc.top,rc.right-rc.left,rc.bottom-rc.top,TRUE);

          //Show the editor
          Edit_CenterTextVertically(g_lpInst->hwndEditor);

          Edit_SetSel(g_lpInst->hwndEditor,0,-1);
          ShowWindow(g_lpInst->hwndEditor,TRUE);

          g_lpInst->fsizeCol = FALSE;
     }
     return FALSE; //Since this is not the standard use of this event don't handle it.
}

BOOL Grid_OnColumnResize(HWND hwnd, LPNMHDR pnm)
{
     //
     // This is called in response to HDN_ITEMCHANGING notification of the
     // WM_NOTIFY message.  It is posted by the list view's header.
     //

     if(!g_lpInst->fResizableHeader) return TRUE;

     ShowWindow(g_lpInst->hwndEditor,FALSE);
     g_lpInst->fsizeCol = TRUE;

     return FALSE;
}

In Grid_OnColumnResize() I hide the edit control. I paint the list view sub item cell the same back color as the edit so the only indication that it is hidden is the lack of a black border. When the sizing operation concludes, I resize and position the editor and re-load and show it. The illusion is of a flicker free perfect real time resize of the edit control!

The list view component may look like it is an owner draw list view but that is not the case. I handle the NM_CUSTOMDRAW notification sent by the list view and do all drawing (including skinning the header) there. For more information about this method, I recommend the following excellent CodeProject article: Neat Stuff to Do in List Controls Using Custom Draw [^].

Drawing a flicker free control is a real challenge. I have seen many articles on this topic and have come to the conclusion that there is no silver bullet that will ensure a flicker free control. Each solution must be tailored to the specific redrawing needs of the control at hand. With the data grid view here, there were two critical events that demanded a problematic redraw of the control. First, if I had row headers in the first column and then clicked the mouse in the grid so that the selected cell changed, I had to invalidate and redraw the first column row headers. This produced annoying flicker in that column. The second cause of flicker occurred whenever I moved the edit around via key board. I had to invalidate the adjacent rows so that there would be a redraw after each key stroke. This flicker was minimal but needed improvement. The solution in my case was to suppress the WM_ERASEBKGND message in the ListView_Proc() under these two conditions.

Providing for Unicode

Updating the Data Grid View to work with Unicode was a fairly straight forward process the particulars of which I document here.

Step 1. Define the UNICODE symbol at the top of the code module.

C++
#define UNICODE

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

//
// Code continues
//

Step 2. Wrap all string literals in the TEXT() macro.

C++
static BOOL Grid_GetInstanceData(HWND hGrid, LPINSTANCEDATA * ppInstanceData)
{
     *ppInstanceData = (LPINSTANCEDATA)GetProp(hGrid, TEXT("lpInsData"));
//
// Code continues
//

Step 3. Convert all fixed length string declarations (EX: char buf[256]) to TCHAR.

C++
BOOL Grid_OnMouseClick(HWND hwnd, LPNMHDR pnm)
{
     static TCHAR buf[2048];
     RECT rc;
//
// Code continues
//

Step 4. Handle the WM_NOTIFYFORMAT message. I went into some detail on this previously here.

C++
//
// Snip previous
//
    case WM_NOTIFYFORMAT:
#ifdef UNICODE
          return NFR_UNICODE;
#else
          return NFR_ANSI;
#endif
//
// Code continues
//

Note: I include the #idef compiler directives here in case I might want to compile this as an ANSI based control.

Providing for 64 Bit

In order to make the control compliant with 64 bit windows, I replaced the calls to Get[Set]WindowLong() with Get[Set]WindowLongPtr().

C++
//
// Snip previous
//
     // Subclass ListView and save the OldProc
     SetProp(hwnd, TEXT("Wprc"), (HANDLE)GetWindowLongPtr(hwnd, GWL_WNDPROC));
     SubclassWindow(hwnd, ListView_Proc);

     return hwnd;
}
C++
static LRESULT CALLBACK Editor_Proc(HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
{
	static TCHAR buf[2048];

	if (WM_DESTROY == msg)	// UnSubclass the Edit Control
	{
		SetWindowLongPtr(hEdit, GWL_WNDPROC, 
				(DWORD)GetProp(hEdit, TEXT("Wprc")));
		RemoveProp(hEdit, TEXT("Wprc"));
		return 0;
	}
//
// Code continues
//

Note: Get[Set]WindowLongPtr() allows for both 32bit and 64bit operation.

History

  • July 13, 2009 version 1.0.0.0
  • July 17, 2009 version 1.0.0.1
    • Updated to provide support for Unicode and 64bit, compilations
    • Improved Editor placement behavior when control is scrolled
  • May 24, 2010
    • Updated download files - fixed bug in demo project

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow do I go about adding this to a project or even compiling it on its own? Pin
Member 1078634330-Apr-14 16:00
Member 1078634330-Apr-14 16:00 
AnswerRe: How do I go about adding this to a project or even compiling it on its own? Pin
David MacDermot1-May-14 7:21
David MacDermot1-May-14 7:21 
QuestionGorgeous! Pin
WiStRoM22-Aug-13 1:46
WiStRoM22-Aug-13 1:46 
GeneralMy vote of 5 Pin
dfbu2k14-Sep-12 7:10
dfbu2k14-Sep-12 7:10 
QuestionGreat article Pin
dfbu2k14-Sep-12 7:01
dfbu2k14-Sep-12 7:01 
GeneralNice Pin
h5iang8-Jul-12 22:56
h5iang8-Jul-12 22:56 
QuestionNice one Pin
Mohibur Rashid23-May-12 17:17
professionalMohibur Rashid23-May-12 17:17 
GeneralClarification on description Pin
john.mcenallay19-May-10 23:14
john.mcenallay19-May-10 23:14 
GeneralRe: Clarification on description Pin
David MacDermot20-May-10 7:49
David MacDermot20-May-10 7:49 
GeneralRe: Clarification on description Pin
john.mcenallay20-May-10 16:22
john.mcenallay20-May-10 16:22 
GeneralRe: Clarification on description Pin
David MacDermot21-May-10 6:36
David MacDermot21-May-10 6:36 
GeneralRe: Clarification on description Pin
john.mcenallay21-May-10 17:17
john.mcenallay21-May-10 17:17 
GeneralAhh. A subtle bug! Pin
David MacDermot24-May-10 8:53
David MacDermot24-May-10 8:53 
In the demo:
C++
BOOL MainDlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
    hList1 = GetDlgItem(hwnd,IDC_GRID);

    //Make some random data
    TCHAR aryByte[2048][3];//256
    srand(11);
    for(int i = 0; i < NELEMS(aryByte); ++i)
        swprintf(aryByte[i], sizeof(TCHAR), TEXT("%02hhX"),rand());

    //Create an array of Column header names
    TCHAR szHdr [17][3];
    for(int Col = -1; Col<17;) //<- ***BUG*** Should be Col<16
        if(-1==Col) wcscpy(szHdr[++Col],TEXT("  "));
        else swprintf(szHdr[++Col],sizeof(TCHAR), TEXT("%02hhX"),Col);

    // Add the columns based on header text
    DataGridView_AddColumns(hList1,szHdr,NELEMS(szHdr));

I was pre-incrementing the variable Col and incremented it beyond the bounds of szHdr[]. I'm using an earlier version of Pelles C and it did not have a problem with this. When I built the demo with the latest version of the compiler I got the same error you did when MainDlg_OnInitDialog returned.


As for the compiler warnings during build: The standard windows macro ListView_GetSubItemRect() triggered those. However the macro functions correctly and is not a problem.


Thanks for the feedback. I'll repost the demo with the fix.


GeneralRe: Ahh. A subtle bug! Pin
john.mcenallay24-May-10 21:39
john.mcenallay24-May-10 21:39 
QuestionNice job. What about Windows 7 ? Pin
Bruno Challier10-Aug-09 6:11
Bruno Challier10-Aug-09 6:11 
AnswerRe: Nice job. What about Windows 7 ? Pin
David MacDermot11-Aug-09 5:50
David MacDermot11-Aug-09 5:50 
GeneralCool Pin
Md. Marufuzzaman20-Jul-09 21:41
professionalMd. Marufuzzaman20-Jul-09 21:41 
Generalhelp , in vc6.0 Pin
mfc1724-Aug-09 4:24
mfc1724-Aug-09 4:24 
AnswerRe: help , in vc6.0 Pin
David MacDermot6-Aug-09 5:45
David MacDermot6-Aug-09 5:45 
Generalkeep it up Pin
Jeremy Falcon16-Jul-09 5:02
professionalJeremy Falcon16-Jul-09 5:02 
GeneralNice but many problems Pin
wlwlxj15-Jul-09 15:07
wlwlxj15-Jul-09 15:07 
GeneralRe: Nice but many problems Pin
David MacDermot22-Jul-09 6:14
David MacDermot22-Jul-09 6:14 
GeneralNice! Pin
jschroedl15-Jul-09 12:39
jschroedl15-Jul-09 12:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.