Win32 SDK 数据网格视图轻松实现






4.88/5 (37投票s)
本文描述了如何创建非MFC自定义控件

目录
引言
这是我多年来撰写的关于Windows SDK开发系列文章的第三篇。前两篇是:
有人曾问我,既然可以使用C++的面向对象能力,为什么还要用C语言编写控件。确实,这里介绍的数据网格视图并非新颖之物,而且市面上已经有不少项目,其中大部分是基于MFC的,并且相当不错。这个问题的答案更多地与我的编程兴趣和个人职业发展有关,而不是其他。
我主要开发用于工程和制造的自动化测试应用程序。我最初使用VB.NET编写工程应用程序,利用供应商提供的框架,这些框架又封装了现有的基于COM的框架,而这些框架又封装了基于C的IO通信框架。我能够快速地搭建一个能工作的应用程序,但它有些慢且臃肿。此外,我从一开始就想做一些供应商框架没有提供支持的事情,而且我找到的许多示例都是用C语言编写的,并直接使用了基于C的IO层。
在我努力学习足够的C语言来满足我的需求,以便适应代码示例的过程中,我发现了BCX - Basic到C转换器 [^],这是一个工具,它允许我获取易懂的Basic代码片段并将其转换为C语言,这样我就可以第一次看到高级语言的抽象层之下。这种自学方法特别有效,因为当我试图解决某个特定问题时,我的兴趣和专注力会达到顶峰——这在阅读教科书或技术出版物时并不一定会发生。
为了巩固我对C语言的理解,我使用Pelles C [^] 编译器和IDE编写了一些基于Windows的应用程序。在这一点上,我开始理解Windows是如何工作的;特别是消息、回调、指针和函数指针。这反过来又帮助我理解了VB.NET和C#在底层是如何工作的。
简而言之,这就是我的旅程。我现在主要使用C#编码,我喜欢它的语法,而且大多数代码示例(VB.NET、JAVA、C++、MFC)都可以轻松地适应我的项目。尽管如此,我在C语言项目中遇到了一些更有趣的挑战和经验。
网格——一个自定义控件
Windows SDK 提供了一小部分基本工具,可以处理大部分用户界面任务,但通常需要一些额外的功能。存在许多网格类型控件的例子证明了这一点。如果微软在其标准SDK控件集合中包含一个网格会很好,但公平地说,操作系统中没有任何可编辑网格的实现。我想要一个基于消息的自定义网格控件,但我不想自己编写和绘制整个东西,所以我查看了一些基于MFC的网格项目,并从中获得了一些关于这个自定义控件的想法。
使用网格
这是最简单的部分。首先在项目中包含自定义控件的头文件。
#include "DataGridView.h"
此数据网格视图是基于消息的自定义控件,因此在使用前必须进行初始化。在演示的WinMain()
方法中,在调用InitCommonControlsEx()
之后立即调用控件的初始化器。
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);
在资源对话框编辑器中,放置一个自定义控件占位符,并为其指定此控件的Windows类名。

消息和宏
使用 Windows 消息配置控件以执行您想要的操作。为了简化此操作并作为消息文档化的一种方式,我为每条消息创建了宏。如果您倾向于显式调用 SendMessage()
或 PostMessage()
,请参阅头文件中的宏定义以了解用法。
COLORREF DataGridView_GetEditorBkColor(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the editor's back color.
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.
COLORREF DataGridView_GetEditorTxtColor(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the editor's text color.
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.
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.
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.
int DataGridView_GetColumnCount(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the number of columns.
int DataGridView_GetRowCount(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the number of rows.
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.
COLORREF DataGridView_GetBkColor(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the data grid view's back color.
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.
COLORREF DataGridView_GetTextColor(
HWND hwnd
);
Parameters
hwnd
Handle to the data grid view control.
Return Values
Returns the data grid view's text color.
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.
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.
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.
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.
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.
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.
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.
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.
除了上述宏之外,我还定义了三个宏,它们简化了网格的设置和数据加载。
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.
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.
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.
创建一个自定义控件
编写自定义控件从定义其窗口类开始(这不要与C++类混淆)。查看DataGridView.c中的InitDataGridView()
函数演示了如何完成此操作。
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);
当调用 CreateWindow()
或 CreateWindowEx()
来创建控件实例时,Windows 会使用这些信息。在演示中,Windows 内部通过调用 CreateWindowEx()
来从资源脚本创建控件。
基于消息的控件的public
接口以控件头文件中定义的Windows消息形式呈现。消息主要通过SendMessage()
Windows API函数传递给控件。SendMessage()
返回控件回调过程返回的结果代码,通过这种方式实现了当今面向对象语言中常见的属性获取器和设置器的功能。
当我着手为这个控件定义消息时,我使用了commctrl.h中找到的列表视图的宏定义作为模式和示例。我的目的是尽可能地模仿标准Microsoft控件的消息。我注意到某些值(BOOL
和INT
)通常被打包到wParam
中,而其他值(struct
指针和string
)通常被打包到lParam
中。这有历史原因 [^],然而现在这两个参数的类型都是相同的。
这里我在一个宏中定义了一个消息及其对应的参数
#define DGVM_SETEDITORTEXTCLR WM_USER + 0x07
#define DataGridView_SetEditorTxtColor(hwnd,clrTxt) \
(BOOL)SNDMSG((hwnd),DGVM_SETEDITORTEXTCLR,0,(LPARAM)(COLORREF)(clrTxt))
管理消息
消息在控件的主回调过程Grid_WndProc()
中处理,其中一部分您可以在这里看到。
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);
}
}
这里有几点我想指出。
首先,我希望与此控件实例一起持久化的所有private
字段,我都保存在一个与此实例链接的结构中。由于代码模块中的所有进程都源于此,我使用Grid_GetInstanceData(hwnd,&g_lpInst)
更新全局指针,使其引用此实例的struct
。这类似于C++中的自引用this
指针或VB中的me
指针。
其次,这个特定的程序很容易失控。我遇到过占用近50%代码模块的回调函数,它们实际上变成了“意大利面条式代码”,消息从一个case分支发送或发布到另一个。发生这种情况时,调试会非常困难,因此我在代码中使用了一些经验法则来保持代码整洁。
- 对所有标准消息使用消息解构器——这些主要在windowsx.h和commctrl.h中找到,并且可以使用向导 [^]轻松实现。消息解构器将消息分配给各种子过程,在那里它们可以被独立处理。
- 如果自定义消息可以在几行代码内处理(所有代码都应在监视器中可见)并且不需要声明变量,则可以在回调过程中处理。换句话说,这些消息应以面向对象语言中处理属性getter或setter的方式对待。如有疑问,则拆分处理。
第三,由于CodeProject和Pelles C社区的一些有益反馈,我加入了一些改进,其中之一就是Unicode支持。请注意,网格现在处理WM_NOTIFYFORMAT
消息。WM_NOTIFY
消息包含一个通知消息代码,该代码可以针对宽字符或ANSI格式。即使使用CreateWindowExW()
创建了控件,ANSI也是默认格式,因此我必须在此明确指定所需的代码格式。(更多关于Unicode的内容稍后。)
控件的私有字段
这是包含我希望与网格控件实例一起持久化的所有数据的struct
。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;
我创建了一些辅助方法,用于在控件生命周期中分配和附加、检索以及销毁这个结构。通过这种方式,控件的实例数量在任何给定时间几乎没有限制。
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;
}
控件的构造函数(某种程度上)
由于Windows类是基于消息的,因此自然会触发一条消息来指示控件正在构建中。WM_CREATE
是我们的控件在创建时接收到的第一批消息之一。WM_CREATE
的消息处理程序充当控件的构造函数。
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);
}
在上面的代码中,我创建了一个列表视图和一个编辑控件作为数据网格视图的子控件。数据网格视图自定义控件类封装了这两个控件,它们发布的任何通知都由数据网格视图控件内部处理。现在我们来看看CreateCellEditor()
。
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;
}
这是一个样板实例化,除了最后几行我对新创建的编辑控件进行了子类化。这将允许我在基于 Windows 消息的控件中覆盖默认行为。具体来说,我将 Editor_Proc()
作为编辑组件所有消息的新目标。我们来看看 Editor_Proc()
。
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);
}
这个过程与顶层的 Grid_WndProc()
在几个方面有所不同。首先注意 WM_DESTROY
是如何处理的。这是样板代码,确保在销毁网格之前,为编辑控件基回调过程指针分配的属性存储被释放。其次,注意最后一行,我们将未处理的消息路由,不是到 DefWindowProc()
,而是到编辑控件的基窗口过程。这两段样板代码作为任何子类化组件回调过程的模板,我们想要覆盖的任何内容都介于它们之间。
到目前为止,我描述了数据网格视图设计的骨干,其中大部分提供了基本的消息路由。这种架构可以应用于任何包含子组件控件的自定义控件。为了使整个系统以自然且具有视觉吸引力的方式协同工作,需要克服几个技术挑战。本文的其余部分将重点关注这些细节。
关注点
列表视图提供可调整大小的列,但调整行的大小并不那么直接。我采用了以下技巧来实现这一点。
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);
}
即使列表中没有图像,列表视图的行也会根据要显示的图像类型进行调整大小。
编辑框需要几乎不可见。我的意思是,当它定位在单元格上方时,文本不应该看起来移动。为了达到这种效果,需要垂直居中编辑框的文本。有一些解决方案,但它们往往相当复杂。我将其精简为以下简单方法。
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);
}
用编辑框和列表视图创建数据网格视图的一个问题是,在鼠标拖动操作中改变列宽时,要确保编辑框尺寸合适。我所看过的那些数据网格视图项目,在这方面处理得不好。通常编辑框会过大或过小,直到通过鼠标点击或键盘导航移动。我花了一些时间观察列表视图及其在列调整大小期间发出的消息,最终发现了一个巧妙的技巧。列表视图的标题会发出HDN_ITEMCHANGING
通知,表明其某个项目开始改变大小。我只需要一个信号,表明调整大小操作已经结束,然后我注意到了以下行为。

请注意,一旦鼠标按钮松开且鼠标位置滑出标题,光标就会改变。这正是调整大小操作完成的完美指示。事实上,如果鼠标按钮按下,无论鼠标在列表视图的何处定位,光标都不会改变(调整大小仍在进行)。此外,如果调整大小已开始,并且鼠标按钮在标题上方短暂松开,则在光标滑出标题之前,调整大小模式不会改变。
在进行了这些观察之后,我编写了以下代码来处理调整大小。
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;
}
在Grid_OnColumnResize()
中,我隐藏了编辑控件。我将列表视图子项单元格绘制成与编辑框相同的背景色,因此唯一的隐藏指示是没有黑色边框。当调整大小操作结束后,我调整并定位编辑器,然后重新加载并显示它。这种错觉是编辑控件的无闪烁完美实时调整大小!
列表视图组件可能看起来是一个所有者绘制的列表视图,但事实并非如此。我处理列表视图发送的NM_CUSTOMDRAW
通知,并在那里进行所有绘制(包括标题皮肤)。有关此方法的更多信息,我推荐以下CodeProject的优秀文章:使用自定义绘制在列表控件中做些很棒的事情 [^]。
绘制一个无闪烁的控件是一项真正的挑战。我见过许多关于这个主题的文章,并得出结论,没有万能的解决方案能确保控件无闪烁。每个解决方案都必须根据当前控件的特定重绘需求进行定制。在这个数据网格视图中,有两个关键事件需要对控件进行有问题的重绘。首先,如果我在第一列有行标题,然后点击网格中的鼠标,导致选定的单元格改变,我必须使第一列的行标题失效并重绘。这在该列中产生了恼人的闪烁。第二个闪烁的原因是每当我通过键盘移动编辑框时。我必须使相邻的行失效,以便在每次按键后进行重绘。这种闪烁很小,但需要改进。我的解决方案是在这两种情况下在ListView_Proc()
中抑制WM_ERASEBKGND
消息。
提供Unicode支持
更新数据网格视图以支持Unicode是一个相当直接的过程,我在此记录其具体细节。
步骤 1. 在代码模块顶部定义 UNICODE 符号。
#define UNICODE
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
//
// Code continues
//
步骤 2. 将所有string
字面量包装在TEXT()
宏中。
static BOOL Grid_GetInstanceData(HWND hGrid, LPINSTANCEDATA * ppInstanceData)
{
*ppInstanceData = (LPINSTANCEDATA)GetProp(hGrid, TEXT("lpInsData"));
//
// Code continues
//
步骤 3. 将所有固定长度的string
声明(例如:char buf[256]
)转换为TCHAR
。
BOOL Grid_OnMouseClick(HWND hwnd, LPNMHDR pnm)
{
static TCHAR buf[2048];
RECT rc;
//
// Code continues
//
步骤 4. 处理 WM_NOTIFYFORMAT
消息。我之前在此处详细介绍了这一点。
//
// Snip previous
//
case WM_NOTIFYFORMAT:
#ifdef UNICODE
return NFR_UNICODE;
#else
return NFR_ANSI;
#endif
//
// Code continues
//
注意:我在此处包含#idef
编译器指令,以防我希望将其编译为基于ANSI的控件。
提供64位支持
为了使控件符合64位Windows,我将对Get[Set]WindowLong()
的调用替换为Get[Set]WindowLongPtr()
。
//
// Snip previous
//
// Subclass ListView and save the OldProc
SetProp(hwnd, TEXT("Wprc"), (HANDLE)GetWindowLongPtr(hwnd, GWL_WNDPROC));
SubclassWindow(hwnd, ListView_Proc);
return hwnd;
}
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
//
注意: Get[Set]WindowLongPtr()
支持 32 位和 64 位操作。
历史
- 2009年7月13日 版本 1.0.0.0
- 2009年7月17日 版本 1.0.0.1
- 更新以支持Unicode和64位编译
- 改进了控件滚动时编辑器的放置行为
- 2010年5月24日
- 更新下载文件 - 修复了演示项目中的错误