User Tools

Site Tools


data_grid

Anatomy of a custom control (a simple data grid)

David MacDermot 2008/09/17 19:55

This article describes how to create a custom control in Pelles C. The control in this case is a data grid like component using a listview and an edit box along with a number of customizations to enhance the appearance of the control.

Project

Introduction

The Windows SDK provides a selection of user interface controls for application development that covers most of the bases when it comes to user interface design. Sooner or later however, you run into a situation where you wish there was something else in the toolbox that isn’t there. A search of the web reveals a number of clever solutions but most of them use MFC or another of the modern object oriented platforms and they don’t translate well into Pelles C. More often than not you are going to have to create your own control from scratch. The good news is rolling your own control does not have to be a difficult proposition.

The windows class

The windows class describes common properties of all windows that will be created using that class. We’ll use the Windows class to encapsulate the methods associated with our custom control and also to provide the public interface for our control. The public interface for our control will be message based so there is only one public method that we create in order to register the windows class. This method is called only once to register the class. Instances of the control are created using CreateWindow().

ATOM 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)); 
    wcex.lpszMenuName    = NULL;
    wcex.lpszClassName   = TEXT("DGridVwClass");
    wcex.hIcon           = NULL;
    wcex.hIconSm         = NULL;
 
    aReturn = RegisterClassEx(&wcex);
 
    return aReturn;
}

This method should be called in the application's entry point procedure immediately following the calls to initialize common controls.

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);
 
    /* Register our custom control*/
    InitDataGridView(hInstance);

The callback procedure

This is where we’ll support the public interface to our control as well as handle the standard windows messages that the operating system sends to our control’s window.

Callback procedures can become quite lengthy and difficult to maintain in a complex control. Windows provides a series of macros in windowsx.h and comctrl.h that greatly enhance code readability and help to break out code into manageable event driven method chunks. These macros are referred to as message crackers and there are some tools that make implementing them fairly easy. I used the Message Cracker Wizard to generate the macros and method prototypes for the standard windows messages, greatly reducing the size of the callback procedure.

Most of the lines of code in this section are devoted to the control’s own custom messages and indeed mimic the property getters and setters common to C++.

static LRESULT CALLBACK Grid_WndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	// Pass along any list view specific messages transparently
	if(LVM_FIRST <= msg && msg <= LVM_FIRST + 181)
	{
		Grid_GetInstanceData(hwnd,&g_lpInst);
		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_HSCROLL, Grid_OnHScroll);
		HANDLE_MSG (hwnd, WM_VSCROLL, Grid_OnVScroll);
		HANDLE_MSG (hwnd, WM_NOTIFY, Grid_OnNotify);
		HANDLE_MSG (hwnd, WM_SETCURSOR, Grid_OnSetCursor);
 
		case DGVM_GETLISTVIEWCONTROL:
			Grid_GetInstanceData(hwnd,&g_lpInst);
			return (LRESULT) g_lpInst->hwndList;
 
		case DGVM_GETEDITCONTROL:
			Grid_GetInstanceData(hwnd,&g_lpInst);
			return (LRESULT) g_lpInst->hwndEditor;
 
		case DGVM_GETCOLUMNCOUNT:
			Grid_GetInstanceData(hwnd,&g_lpInst);
			return (LRESULT) Grid_GetColumnCount(g_lpInst->hwndList);
                //
                // Skip a bunch of properties for the sake of brevity
                //
		case DGVM_SETROWHEIGHT:
			Grid_GetInstanceData(hwnd,&g_lpInst);
			Grid_SetRowHeight(g_lpInst->hwndList, (INT) wParam);
			return TRUE;
 
		default: return DefWindowProc (hwnd, msg, wParam, lParam);
	}
}

The public interface

We’ll define the public interface in the control’s header file.

Windows reserves a block of messages for custom controls beginning with the definition of WM_USER so our messages will be WM_USER + some value we assign. Messages are sent to a control via SendMessage() which gives us two parameters that we can use for passing values to the control’s callback procedure. We can also obtain the callback return value from SendMessage(). I find it helpful to define macros for each message as I define the messages themselves. This serves as a way of documenting each message in the code of the header file during development and provides a syntactically pleasing interface for accessing the control’s properties in source code.

In addition to the message macros I have also defined some helper macros to make it easier to add columns and rows to our control. Notice that these macros do not contain references to our custom messages! In the control’s callback procedure any listview specific message is forwarded to the grid control’s listview component.

/****************************************************************************/
// Messages
 
#define DGVM_GETLISTVIEWCONTROL WM_USER + 0x01
#define DGVM_GETEDITCONTROL WM_USER + 0x02
#define DGVM_GETCOLUMNCOUNT WM_USER + 0x03
#define DGVM_GETEDITORBACKCLR WM_USER + 0x04
#define DGVM_SETEDITORBACKCLR WM_USER + 0x05
#define DGVM_GETEDITORTEXTCLR WM_USER + 0x06
#define DGVM_SETEDITORTEXTCLR WM_USER + 0x07
#define DGVM_RESIZEABLEHEADER WM_USER + 0x08
#define DGVM_GETALTBACKCLR WM_USER + 0x09
#define DGVM_SETALTBACKCLR WM_USER + 0x0A
#define DGVM_GETALTTXTCLR WM_USER + 0x0B
#define DGVM_SETALTTXTCLR WM_USER + 0x0C
#define DGVM_SETDOUBLECLICKSELECT WM_USER + 0x0D
#define DGVM_FIRSTCOLUMNROWHEADERS WM_USER + 0x0E
#define DGVM_SETROWHEIGHT WM_USER + 0x0F
 
/****************************************************************************/
// Macroes
 
#define DataGridView_AddColumn(hGrid,nCol,iWidth,szColText){ \
	LVCOLUMN lv_c; lv_c.mask=LVCF_TEXT|LVCF_WIDTH|LVCF_SUBITEM; \
	lv_c.cx=iWidth; lv_c.pszText=(szColText); \
	ListView_InsertColumn(hGrid,nCol,&lv_c);}
 
#define DataGridView_AddColumns(hGrid,aryColTxt,iColCount) \
	for(int Col = 0; Col<iColCount;Col++) \
		DataGridView_AddColumn((hGrid),Col,strlen(aryColTxt[Col])*16,aryColTxt[Col]);
 
#define DataGridView_AddRow(hGrid,aryItemTxt,iColCount) \
	for(int Col = 0; Col<iColCount;){\
		LV_ITEM Lv_i; \
		Lv_i.mask=LVIF_TEXT; \
		if(0==Col) { \
			Lv_i.iItem=ListView_GetItemCount(hGrid); \
			Lv_i.iSubItem=Col; Lv_i.pszText=aryItemTxt[Col++]; \
	 		ListView_InsertItem(hGrid,&Lv_i);} \
		Lv_i.iSubItem=Col; \
		Lv_i.pszText=aryItemTxt[Col++]; \
		ListView_SetItem(hGrid,&Lv_i);}
 
#define DataGridView_GetEditorBkColor(hwnd)  (COLORREF)SNDMSG((hwnd),DGVM_GETEDITORBACKCLR,0,0L)
#define DataGridView_SetEditorBkColor(hwnd,clrBk)  (BOOL)SNDMSG((hwnd),DGVM_SETEDITORBACKCLR,0,(LPARAM)(COLORREF)(clrBk))
#define DataGridView_GetEditorTxtColor(hwnd)  (COLORREF)SNDMSG((hwnd),DGVM_GETEDITORTEXTCLR,0,0L)
#define DataGridView_SetEditorTxtColor(hwnd,clrBk)  (BOOL)SNDMSG((hwnd),DGVM_SETEDITORTEXTCLR,0,(LPARAM)(COLORREF)(clrBk))
#define DataGridView_GetListViewControl(hwnd)  (HWND)SNDMSG((hwnd),DGVM_GETLISTVIEWCONTROL,0,0L)
#define DataGridView_GetEditControl(hwnd)  (HWND)SNDMSG((hwnd),DGVM_GETEDITCONTROL,0,0L)
#define DataGridView_GetColumnCount(hwnd)  (INT)SNDMSG((hwnd),DGVM_GETCOLUMNCOUNT,0,0L)
#define DataGridView_GetRowCount(hwnd)  ListView_GetItemCount((hwnd))
#define DataGridView_SetResizableHeader(hwnd,fResizable)  (BOOL)SNDMSG((hwnd),DGVM_RESIZEABLEHEADER,(WPARAM)(fResizable),(LPARAM)0L)
#define DataGridView_GetAltBkColor(hwnd)  (COLORREF)SNDMSG((hwnd),DGVM_GETALTBACKCLR,0,0L)
#define DataGridView_SetAltBkColor(hwnd,clrBk,fPaintByRow)  (BOOL)SNDMSG((hwnd),DGVM_SETALTBACKCLR,(WPARAM)(fPaintByRow),(LPARAM)(COLORREF)(clrBk))
#define DataGridView_GetAltTextColor(hwnd)  (COLORREF)SNDMSG((hwnd),DGVM_GETALTTXTCLR,0,0L)
#define DataGridView_SetAltTextColor(hwnd,clrBk)  (BOOL)SNDMSG((hwnd),DGVM_SETALTTXTCLR,0,(LPARAM)(COLORREF)(clrBk))
#define DataGridView_SetDoubleClickSelect(hwnd,fDblClk)  (BOOL)SNDMSG((hwnd),DGVM_SETDOUBLECLICKSELECT,(WPARAM)(fDblClk),0L)
#define DataGridView_FirstColumnRowHeaders(hwnd,fDspRwHdr)  (BOOL)SNDMSG((hwnd),DGVM_FIRSTCOLUMNROWHEADERS,(WPARAM)(fDspRwHdr),0L)
#define DataGridView_SetRowHeight(hwnd,iHeight)  (BOOL)SNDMSG((hwnd),DGVM_SETROWHEIGHT,(WPARAM)(iHeight),0L)
 
/****************************************************************************/
// Exported function prototypes
 
BOOL InitDataGridView(HINSTANCE hInstance);

Keeping track of multiple instances

I use a struct to persist properties of each instance of the custom control.

typedef struct _tagINSTANCEDATA{
	HINSTANCE hInstance;
	HWND hwndList;
	HWND hwndEditor;
	LVHITTESTINFO hti;
	SCROLLINFO hsInfo;
	SCROLLINFO vsInfo;
	COLORREF Editor_TxtColr;
	COLORREF Editor_BkColr;
	COLORREF Alt_TxtColr;
	COLORREF Alt_BkColr;
	BOOL fPaintByRow;
	BOOL fRowHeaders;
	BOOL fResizableHeader;
	BOOL fDblClick;
}INSTANCEDATA, *LPINSTANCEDATA;

In addition to this there are some standard methods that I use to manage the creation and assignment of, access to, and destruction of this structure.

static BOOL Control_GetInstanceData (HWND hControl, LPINSTANCEDATA *ppInstanceData)
{
	*ppInstanceData=(LPINSTANCEDATA)GetProp(hControl,(LPCSTR)"lpInsData");
	return NULL != *ppInstanceData;
}
 
static BOOL Control _CreateInstanceData (HWND hControl,  LPINSTANCEDATA pInstanceData)
{
	LPINSTANCEDATA pInst = (LPINSTANCEDATA)malloc(sizeof(INSTANCEDATA));
	memmove(pInst,pInstanceData,sizeof(INSTANCEDATA));
 
	return SetProp(hControl,"lpInsData",pInst);
}
 
static BOOL Control _FreeInstanceData (HWND hControl)
{
	LPINSTANCEDATA pInst;
	if(Control _GetInstanceData(hGrid,&pInst))
	{
		free ((LPINSTANCEDATA)pInst);
		RemoveProp(hControl,"lpInsData");
		return TRUE;
	}
	return FALSE;
}

Here I create and assign the structure in the WM_CREATE handler of the custom control

 static BOOL Grid_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
	INSTANCEDATA inst;
 
	// initialize hit test info and scroll info
	memset(&inst.hti, -1, sizeof(LVHITTESTINFO));
	memset(&inst.hsInfo, 0, sizeof(SCROLLINFO));
	memset(&inst.vsInfo, 0, sizeof(SCROLLINFO));
 
	// get the hInstance
	inst.hInstance = lpCreateStruct->hInstance;
 
    // create the ListView control
    inst.hwndList = CreateListView(lpCreateStruct->hInstance, hwnd);
	if(NULL == inst.hwndList) return FALSE;
 
	// default ListView Colors
	inst.fPaintByRow = TRUE;
	inst.Alt_TxtColr = ListView_GetTextColor(inst.hwndList);
	inst.Alt_BkColr = ListView_GetBkColor(inst.hwndList);
 
	// default ListView pseudoHeaders off
	inst.fRowHeaders = FALSE;
 
    inst.hwndEditor = CreateCellEditor(lpCreateStruct->hInstance, hwnd);
	if(NULL == inst.hwndEditor) return FALSE;
 
	// default Cell Editor Colors
	inst.Editor_BkColr = RGB(0,0,160);
	inst.Editor_TxtColr = RGB(255,255,255);
 
	// default Cell selection behavior
	inst.fDblClick = FALSE;
 
	Grid_SetRowHeight(inst.hwndList, 20);
 
	// TODO: Create inializations for an array of cell edit controls
 
	return Grid_CreateInstanceData(hwnd,&inst);
}

Here I set a global pointer to the struct associated with this instance of the control and then free the structure when it is no longer needed.

 static VOID Grid_OnDestroy(HWND hwnd)
{
	Grid_GetInstanceData(hwnd,&g_lpInst);
 
	DestroyWindow(g_lpInst->hwndList);
	DestroyWindow(g_lpInst->hwndEditor);
 
	Grid_FreeInstanceData(hwnd);
	PostQuitMessage(0);
}

Points of interest

Custom drawing the listview

All of the customization and drawing, of the grid’s components, is handled in the NM_CUSTOMDRAW notification of the list view, this includes skinning the header, something that normally one cannot do, but is possible because the header is subclassed. For a detailed tutorial about simple customizations using this method see Neat Stuff to Do in List Controls Using Custom Draw.

LRESULT Grid_OnCustomDraw (HWND hwnd, LPNMLVCUSTOMDRAW lplvcd)
{
	Grid_GetInstanceData(hwnd,&g_lpInst);
 
    switch(lplvcd->nmcd.dwDrawStage) 
    {
		//Before the paint cycle begins
		//request notifications for individual listview items
        case CDDS_PREPAINT : return CDRF_NOTIFYITEMDRAW;
 
		//Before an item is drawn
 		//request notifications for individual listview subitems
        case CDDS_ITEMPREPAINT: return CDRF_NOTIFYSUBITEMDRAW;
 
        case CDDS_SUBITEM | CDDS_ITEMPREPAINT:
        {
			// Color alternating Rows
			if(g_lpInst->fPaintByRow&&lplvcd->nmcd.dwItemSpec & 1)
			{
				lplvcd->clrText = g_lpInst->Alt_TxtColr;
				lplvcd->clrTextBk = g_lpInst->Alt_BkColr;
			}
			// Color alternating Cols
			else if(!g_lpInst->fPaintByRow&&lplvcd->iSubItem & 1)
			{
				lplvcd->clrText   = g_lpInst->Alt_TxtColr;
				lplvcd->clrTextBk = g_lpInst->Alt_BkColr;
			}
			else // Color default
			{
				lplvcd->clrText   = ListView_GetTextColor(g_lpInst->hwndList);
				lplvcd->clrTextBk = ListView_GetBkColor(g_lpInst->hwndList);
			}
			// Color active cell
			if( lplvcd->iSubItem == g_lpInst->hti.iSubItem &&
				lplvcd->nmcd.dwItemSpec == g_lpInst->hti.iItem)
			{
				lplvcd->clrText   = g_lpInst->Editor_TxtColr;
				lplvcd->clrTextBk = g_lpInst->Editor_BkColr;
			}
			// Skin Column headers
			if(g_lpInst->fRowHeaders)
			{
				HWND hHdr;
				HDITEM hdi;
				RECT rc;
				HDC hdc;
 
				hHdr = ListView_GetHeader(hwnd);
				Header_GetItem(hHdr,lplvcd->iSubItem, &hdi);
				Header_GetItemRect(hHdr,lplvcd->iSubItem, &rc);
				hdc=GetDC(hHdr);
 
				if(lplvcd->iSubItem == g_lpInst->hti.iSubItem)
					DrawEdge(hdc, &rc,EDGE_SUNKEN,BF_RECT|BF_SOFT|BF_MIDDLE);
				else
					DrawEdge(hdc, &rc,EDGE_RAISED,BF_RECT|BF_SOFT|BF_MIDDLE);
 
				rc.left+=6;
				rc.bottom-=2;
 
				SetBkMode(hdc,TRANSPARENT);
				SetTextColor(hdc,GetSysColor(COLOR_BTNTEXT));
				SelectObject(hdc,GetStockObject(DEFAULT_GUI_FONT));
				DrawText(hdc,hdi.pszText,lstrlen(hdi.pszText), &rc,DT_SINGLELINE|DT_BOTTOM|DT_LEFT);
				SetBkMode(hdc,OPAQUE);
 
				ReleaseDC(g_lpInst->hwndList,hdc);
			}
			// Draw Row headers
			if(lplvcd->iSubItem == 0&&g_lpInst->fRowHeaders)
			{
				static char buf [2048];
				RECT  rc;
				HDC  hdc;
 
				ListView_GetSubItemRect(g_lpInst->hwndList,
					lplvcd->nmcd.dwItemSpec,0,LVIR_LABEL,&rc);
 
				rc.left-=2;
				rc.top-=1;
				rc.bottom-=1;
 
				hdc=GetDC(g_lpInst->hwndList);
 
				if(lplvcd->nmcd.dwItemSpec==g_lpInst->hti.iItem)
					DrawEdge(hdc, &rc,EDGE_SUNKEN,BF_RECT|BF_SOFT|BF_MIDDLE);
				else
					DrawEdge(hdc, &rc,EDGE_RAISED,BF_RECT|BF_SOFT|BF_MIDDLE);
 
				ListView_GetItemText(g_lpInst->hwndList,lplvcd->nmcd.dwItemSpec,0,buf,sizeof buf);
 
				SetBkMode(hdc,TRANSPARENT);
				SetTextColor(hdc,GetSysColor(COLOR_BTNTEXT));
				SelectObject(hdc,GetStockObject(DEFAULT_GUI_FONT));
				DrawText(hdc,buf,lstrlen(buf), &rc,DT_SINGLELINE|DT_VCENTER|DT_CENTER);
				SetBkMode(hdc,OPAQUE);
 
				ReleaseDC(g_lpInst->hwndList,hdc);
 
				return CDRF_SKIPDEFAULT;
			}
			return CDRF_NEWFONT;
		}
    }
    return CDRF_DODEFAULT;
}

Getting the column count

The listview has a method for getting the item count (row count) but does not provide a method for getting column count. It is therefore necessary to provide an indirect way of determining column count. One method would be to get an item count from the listview’s header but this does not work if the header is not visible yet. The following is a method that works under any circumstances.

static INT ListView_GetColumnCount(HWND hwnd)
{
	// Method: hittest low right corner of row
	BOOL fTempRow = FALSE;
	RECT rc;
	LVHITTESTINFO ht = {0};
 
	if(0 == ListView_GetItemCount(hwnd)) // No rows yet
	// Temporarily add a row in order to get the count
	{
		LV_ITEM Lv_i = {LVIF_TEXT,0,0,0,0,""};
		ListView_InsertItem(hwnd,&Lv_i);
		fTempRow = TRUE;
	}
	ListView_GetItemRect(hwnd,
		ListView_GetItemCount(hwnd)-1,
		&rc,LVIR_BOUNDS);
	ht.pt.x = rc.right-1;
	ht.pt.y = rc.bottom-1;
	ListView_SubItemHitTest(hwnd, &ht);
 
	if(fTempRow) ListView_DeleteItem(hwnd,0); //Delete temp row (if any)
 
	return ht.iSubItem;
}

Positioning the editor on resize

Resizing a column is fairly straight forward. But what happens to the edit control that I am positioning over the grid during a resize? It must resize and position accordingly. This is hard to do real time but with some smoke and mirrors I was able to make it look like that is just what is happening.

The listview's header posts an HDN_ITEMCHANGING notification that signals a resize has begun. I hide the edit control when this happens but the underlying subitem is painted the same colors as the edit control so it looks like the edit control is stretching real-time. All I need to do when the resize is complete is reposition the edit control and show it again. Unfortunately there is no direct way to determine that a resize is complete.

I must confess that this one had me stumped for a while. I played with the list view and observed closely what was happening during a resize.

  • Mouse down on header
  • Mouse move
  • Mouse up

After attempting a mouse based approach with limited success I noticed something else: the cursor changes when resizing and after a resize is completed. I now had an easy way to determine the completion of the resize.

static BOOL  Grid_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
{
	//
	// If this is a resizable grid, initiate column resizing
	//  resizing is concluded in the Grid_OnSetCursor event handler
	//
	if(HDN_ITEMCHANGING==pnm->code)
		return Grid_OnColumnResize(hwnd, pnm);
BOOL Grid_OnColumnResize(HWND hwnd, LPNMHDR pnm)
{
	Grid_GetInstanceData(hwnd,&g_lpInst);
 
	if(!g_lpInst->fResizableHeader) return TRUE;
 
	ShowWindow(g_lpInst->hwndEditor,FALSE);
	g_fsizeCol = TRUE;
 
	return FALSE;
}
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_fsizeCol)
	{
		RECT rc;
		Grid_GetInstanceData(hwnd,&g_lpInst);
 
		//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
		ShowWindow(g_lpInst->hwndEditor,TRUE);
 
		g_fsizeCol = FALSE;
	}
	return FALSE; //Since this is not the standard use of this event don't handle it.
}

Scrolling

Getting the scrolling to work right in this control was a challenge. I would resize the grid columns so that the horizontal scrollbar appeared, scroll over, then resize the columns back to the initial size. This caused the scrollbar to disappear leaving the view shifted and some of the control not visible. The solution was to persist the vertical and horizontal scroll info in the control so that it wasn't lost when the bars vanished. In the case of horizontal scrolling, the control pans to the origin if the size of the listview reduces to fit within the parent window. Vertical scrolling units reflect row height and horizontal units are set by the width of the first visible column. I find that this gives the control the smoothest scrolling behavior while avoiding widowed or orphaned rows. Especially when navigating the control from the keyboard.

Horizontal scrolling:

void Grid_OnHScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
{
	Grid_GetInstanceData(hwnd,&g_lpInst);
	RECT rc;
	GetClientRect(hwnd,&rc);
	int line = ListView_GetFirstVisibleColWidth(g_lpInst->hwndList, &rc);
	int page = line * ((int)(rc.right-rc.left) / line);
 
	int oldPos = g_lpInst->hsInfo.nPos;
 
	switch (code)
	{
		case SB_BOTTOM:			//Scrolls to the lower right.
			g_lpInst->hsInfo.nPos = g_lpInst->hsInfo.nMax;
			break;
 
		case SB_TOP:			//Scrolls to the lower left.
			g_lpInst->hsInfo.nPos = g_lpInst->hsInfo.nMin;
			break;
 
		case SB_ENDSCROLL:		//Ends scroll.
			break;
 
		case SB_LINELEFT:		//Scrolls left by one unit.
			g_lpInst->hsInfo.nPos -= line;
			break;
 
		case SB_LINERIGHT:		//Scrolls right by one unit.
			g_lpInst->hsInfo.nPos += line;
			break;
 
		case SB_PAGELEFT:		//Scrolls left by the width of the window.
			g_lpInst->hsInfo.nPos -= page;
			break;
 
		case SB_PAGERIGHT:		//Scrolls right by the width of the window.
			g_lpInst->hsInfo.nPos += page;
			break;
 
		case SB_THUMBPOSITION:	//Scrolls to the absolute position. The current position is specified by the Pos parameter.
			g_lpInst->hsInfo.nPos = pos;
			break;
 
		case SB_THUMBTRACK:		//Drags scroll box to the specified position. The current position is specified by the Pos parameter.
			g_lpInst->hsInfo.nPos = pos;	
			break;				
	}
	// Ensure that we do not scroll beyond visible boundaries
	g_lpInst->hsInfo.nPos = g_lpInst->hsInfo.nPos < g_lpInst->hsInfo.nMin ||
		(code == SB_LINELEFT && g_lpInst->hsInfo.nPos < line) ?
			 g_lpInst->hsInfo.nMin : g_lpInst->hsInfo.nPos;
	g_lpInst->hsInfo.nPos = g_lpInst->hsInfo.nPos > g_lpInst->hsInfo.nMax - g_lpInst->hsInfo.nPage ?
		 g_lpInst->hsInfo.nMax - g_lpInst->hsInfo.nPage : g_lpInst->hsInfo.nPos;
 
	g_lpInst->hsInfo.fMask = SIF_POS;
	SetScrollInfo(hwnd,SB_HORZ,&g_lpInst->hsInfo,TRUE);
	Scroll(hwnd,-(g_lpInst->hsInfo.nPos-oldPos),0);
}

Vertical scrolling:

static VOID Grid_OnVScroll(HWND hwnd, HWND hwndCtl, UINT code, INT pos)
{
	Grid_GetInstanceData(hwnd,&g_lpInst);
	RECT rc;
	GetClientRect(hwnd,&rc);
	int line = ListView_GetRowHeight(g_lpInst->hwndList, &rc);
	int page = line * ((int)(rc.bottom-rc.top) / line);
 
	pos = line * (pos / line);//ScrollPos in increments of line
 
	int oldPos = g_lpInst->vsInfo.nPos;
 
	switch (code)
	{
		case SB_BOTTOM:			//Scrolls to the lower right.
			g_lpInst->vsInfo.nPos = g_lpInst->vsInfo.nMax;
			break;
 
		case SB_TOP:			//Scrolls to the upper left.
			g_lpInst->vsInfo.nPos = g_lpInst->vsInfo.nMin;
			break;
 
		case SB_ENDSCROLL:		//Ends scroll.
			break;
 
		case SB_LINEDOWN:		//Scrolls one line down.
			g_lpInst->vsInfo.nPos += line;
			break;
 
		case SB_LINEUP:			//Scrolls one line up.
			g_lpInst->vsInfo.nPos -= line;
			break;
 
		case SB_PAGEDOWN:		//Scrolls one page down.
			g_lpInst->vsInfo.nPos += page;
			break;
 
		case SB_PAGEUP:			//Scrolls one page up.
			g_lpInst->vsInfo.nPos -= page;
			break;
 
		case SB_THUMBPOSITION:			//Scrolls to the absolute position. The current position is specified by the nPos parameter.
			g_lpInst->vsInfo.nPos = pos;
			break;
 
		case SB_THUMBTRACK:			//Drags scroll box to the specified position. The current position is specified by the nPos parameter.
			g_lpInst->vsInfo.nPos = pos;
			break;							
	}
	g_lpInst->vsInfo.nPos = g_lpInst->vsInfo.nPos < g_lpInst->vsInfo.nMin ? g_lpInst->vsInfo.nMin : g_lpInst->vsInfo.nPos;
	g_lpInst->vsInfo.nPos = g_lpInst->vsInfo.nPos > g_lpInst->vsInfo.nMax - g_lpInst->vsInfo.nPage ? g_lpInst->vsInfo.nMax - g_lpInst->vsInfo.nPage : g_lpInst->vsInfo.nPos;
 
	// In order to reduce flicker in the header we position the header
	// before or after the scroll depending on the direction of scroll
 
	if(oldPos < pos) PinHeaderToTop(ListView_GetHeader(g_lpInst->hwndList)); 
 
	g_lpInst->vsInfo.fMask = SIF_POS;
	SetScrollInfo(hwnd,SB_VERT,&g_lpInst->vsInfo,TRUE);
	Scroll(hwnd,0,-(g_lpInst->vsInfo.nPos-oldPos));
 
	if(oldPos > pos)PinHeaderToTop(ListView_GetHeader(g_lpInst->hwndList));
}

Calling the scroll events from the ListView_KeyProc so the edit box stays in view

static LRESULT CALLBACK ListView_KeyProc (HWND hList, UINT msg, WPARAM wParam, LPARAM lParam)
{
	// Note: Instance data is attached to ListView's parent
	static RECT rc, rcParent;
	static char buf[2048];
 
	if(WM_DESTROY==msg) // UnSubclass the ListView Control
	{
		SetWindowLong(hList,GWL_WNDPROC,(DWORD)GetProp(hList,"Wprc"));
		RemoveProp(hList,"Wprc");
		return 0;
	}
	else if(WM_KEYDOWN==msg)
	{
		HWND hParent = GetParent(hList);
 
		//
		//  Snip
		//
 
		//Adjust Scrolls
		switch(wParam)
		{
			case VK_HOME:
					SNDMSG(hParent,WM_HSCROLL,SB_TOP,0);
				break;
			case VK_END:
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.right)))
					SNDMSG(hParent,WM_HSCROLL,SB_BOTTOM,0);
				break;
			case VK_PRIOR:
					SNDMSG(hParent,WM_VSCROLL,SB_TOP,0);
				break;
			case VK_NEXT:
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.right)))
					SNDMSG(hParent,WM_VSCROLL,SB_BOTTOM,0);
				break;
			case VK_UP:
			{
				RECT rcHeader;
				GetClientRect(ListView_GetHeader(hList),&rcHeader);
 
				//Keep the edit box from overlaying the header bar
				rcParent.top += (rcHeader.bottom-rcHeader.top);
 
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.left)))
					SNDMSG(hParent,WM_VSCROLL,SB_LINEUP,0);
			}
				break;
			case VK_DOWN:
				rcParent.bottom -= ListView_GetRowHeight(hList, &rcParent);
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.left)))
					SNDMSG(hParent,WM_VSCROLL,SB_LINEDOWN,0);
				break;
			case VK_LEFT:
				rcParent.left += ListView_GetFirstVisibleColWidth(hList, &rcParent);
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.right)))
					SNDMSG(hParent,WM_HSCROLL,SB_LINELEFT,0);
				break;
			case VK_RIGHT:
				rcParent.right -= ListView_GetLastVisibleColWidth(hList, &rcParent);
				if(!PtInRect(&rcParent,*((LPPOINT)&rc.left)))
					SNDMSG(hParent,WM_HSCROLL,SB_LINERIGHT,0);
				break;
		}
		return 0;
	}
data_grid.txt · Last modified: 2008/09/19 01:09 by dmac