VC6.0中基于MSFlexGrid的可编辑表格
的制作及其若干问题的解决方案
一、 概述
在我们制作的用户界面中很多时候会用到表格,当然我们首先想到的是用控件,如MSFlexGrid。我们可以方便的调用控件自身的函数来对表格中的元素进行操作,但是如果要设计一个可以编辑的表格,我们该怎么办呢?事实上这种可编辑表格的应用还真的不少,主要是其用户操作的交互性较好。笔者在前一阶段开发项目时遇到了这个问题,以下介绍笔者的实现方案。
二、 可编辑表格的初步实现
1、 创建新类CCtrlEditGrid
首先创建一个单文档工程EditGrid。
接着在工程中加入MSFlexGrid控件。这是个ActiveX控件,选择AddToProject的Components and Controls Gallery选项可加入该控件。
然后以MSFlexGrid为基类创建新类CCtrlEditGrid,并添加成员函数void InitGrid()(该函数目前只是空的)和成员变量 CEdit* m_pEdit;
CSpinButtonCtrl* m_pSpinButtonCtrl;以后表格的实体类就是该类。
2、 在工程文件的视图类中显示表格
首先在视图类CEditGridView中添加成员变量CCtrlEditGrid* m_pCtrlEditGrid。
接着添加CEditGridView的消息相应函数OnCreate,在其中创建表格
m_pCtrlEditGrid = new CCtrlEditGrid;
m_pCtrlEditGrid->Create(NULL,WS_CHILD|WS_VISIBLE,CRect(0,0,0,0),this,ID_EDITGRID);
m_pCtrlEditGrid->InitGrid ( );
然后为了和视图的大小保持一致在CEditGridView的消息相应函数OnSize中添加代码
if ( m_pCtrlEditGrid != NULL )
m_pCtrlEditGrid->MoveWindow(0,0,cx,cy);
3、 实现CCtrlEditGrid的InitGrid的函数
InitGrid完成表格的属性设置,表格初始内容的填写,可编辑控件的创建。这里的可编辑控件如CEdit,CComboBox,CSpinButtonCtrl,CDateTimeCtrl……。在本例中只使用CEdit和CSinButtonCtrl的结合这一种。如果表格中不同列之间的编辑控件不同,在程序中可以通过检测列号,来决定使用什么控件,事实上在笔者的项目中不同列之间也是使用不同编辑控件的。在此用一种控件来说明表格编辑的实现方法,读者想换其他的控件也很容易了。
void CCtrlEditGrid::InitGrid()
{
//设置行数,列数
SetCols( COL_INITNUMBER );
SetRows( ROW_INITNUMBER );
//设置为无边框
SetBorderStyle(0);
//设置为可以改变行高列宽
SetAllowUserResizing(3);
//设置行宽列宽
CDC* pDC = GetDC();
SetRowHeightMin ( ( long )( ROW_HEIGHT_PIXEL * 1440.0 / pDC->GetDeviceCaps(LOGPIXELSY)) );//坐标单位要转换
for ( int i = 0; i < COL_INITNUMBER; i++ )
SetColWidth ( i,( long ) ( COL_WIDTH_INDEX_PIXEL * 1440.0 / pDC->GetDeviceCaps(LOGPIXELSX) ) );
ReleaseDC(pDC);
//设置列的文字对齐方式
for ( i = 0 ; i < COL_INITNUMBER ; i ++ )
SetColAlignment ( i, 4 );
//设置固定行列的名称
for ( i = 1; i < ROW_INITNUMBER; i ++ )
{
CString strNum;
int nNum = i;
strNum.Format ("%d", i);
SetTextMatrix (i, 0, strNum );
}
for ( i = 1; i < COL_INITNUMBER; i ++ )
{
CString strNum;
int nNum = i;
strNum.Format ("%d", i);
SetTextMatrix (0, i, strNum );
}
//填充Grid中的原始内容
for ( i = 1; i < ROW_INITNUMBER; i ++ )
{
for ( int j = 1; j < COL_INITNUMBER; j ++ )
{
SetTextMatrix (i, j, "1" );
}
}
//创建控件
m_pEdit = new CEdit();
m_pEdit->Create(WS_CHILD|WS_BORDER|ES_AUTOHSCROLL|ES_NUMBER,CRect(0,0,0,0),this,ID_CTRL_EDIT);
m_pSpinButtonCtrl = new CSpinButtonCtrl();
m_pSpinButtonCtrl->Create (UDS_ARROWKEYS | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | WS_BORDER ,
CRect(0,0,0,0),this,ID_CTRL_SPIN);
这里要解释说明的是MSFLEXGRID中的单位是缇(1缇=1/1440in),所以在我们要指定单元格的长宽等时,要将像素单位转换为缇。如ROW_HEIGHT_PIXEL * 1440.0 / pDC->GetDeviceCaps(LOGPIXELSY)。其中ROW_HEIGHT_PIXEL是一个表示行高的像素单位的宏,pDC->GetDeviceCaps(LOGPIXELSY)得到Y轴每inch的像素值。该式计算后的值就是相应的以缇为单位的值。
4、 让表格可以编辑
以上三点只是准备阶段,要想使表格编辑,我们还要响应用户的点击单元格事件和离开单元格事件,以使得当用户点击某一单元格时当前单元格处于编辑状态而离开时又处于非编辑状态。MSFLEXGRID控件提供的OnClick和OnLeaveCell事件正好是我们所需要的。由于CCtrlEditGrid不是MFC类,所以不能用类向导来添加事件。只好用手工添加了。
首先在头文件中添加afx_msg void OnLeaveCell();afx_msg void OnClick();接着在CPP文件中添加事件映射表
BEGIN_EVENTSINK_MAP(CCtrlEditGrid, CMSFlexGrid)
//{{AFX_EVENTSINK_MAP(CEditGrid)
ON_EVENT_REFLECT(CCtrlEditGrid, 72 /* LeaveCell */, OnLeaveCell, VTS_NONE)
ON_EVENT_REFLECT(CCtrlEditGrid, -600 /* Click */, OnClick, VTS_NONE)
//}}AFX_EVENTSINK_MAP
END_EVENTSINK_MAP()
(如果用户觉得手工添加时间映射表有困难,可以先在应用程序中添加一个虚设对话框。接着在对话框中插入MSFlexGrid控件。然后使用ClassWizard将事件处理程序写入对话框,接下来就可以参照着对话框编写事件映射表了。记得最后要删除虚设对话框。)
接着添加OnLeaveCell和OnClick的函数体。
OnLeaveCell函数:如果现在m_pEdit是显示的,则说明单元格是在编辑状态,所以要将数据从m_pEdit框读到表格中,然后将m_pEdit和m_pSpinButtonCtrl隐藏。
void CCtrlEditGrid::OnLeaveCell()
{
if ( m_pEdit->IsWindowVisible() )
{
int nCol;
int nRow;
CString strContent;
nCol = GetCol();
nRow = GetRow();
m_pEdit->GetWindowText(strContent);
SetTextMatrix(nRow, nCol, strContent);
m_pEdit->ShowWindow(SW_HIDE);
m_pSpinButtonCtrl->ShowWindow(SW_HIDE);
}
}
OnClick函数:要在点击的单元格中显示m_pEdit和m_pSpinButtonCtrl,,并使输入焦点在m_pEdit中,这里要说明的一点是在计算编辑控件要显示的位置时,如果FlexGrid控件有边框,就应该考虑边框宽度对位置的影响,在本例中我们在InitGrid中设置为无边框,故不用考虑。
void CCtrlEditGrid::OnClick()
{
CDC* pDC = GetDC();
long x = ( GetCellLeft() * pDC -> GetDeviceCaps ( LOGPIXELSX ) ) / 1440;
long y = ( GetCellTop() * pDC -> GetDeviceCaps ( LOGPIXELSY ) ) / 1440;
long cx = ( GetCellWidth() * pDC -> GetDeviceCaps ( LOGPIXELSX ) ) / 1440;
long cy = ( GetCellHeight() * pDC -> GetDeviceCaps ( LOGPIXELSY ) )/ 1440;
ReleaseDC ( pDC );
CString strContent;
strContent = GetText();
m_pEdit->SetWindowText(strContent);
m_pEdit->MoveWindow(x,y,cx,cy,FALSE);
m_pEdit->ShowWindow(SW_SHOW);
m_pEdit->SetFocus ( );
m_pSpinButtonCtrl->SetBuddy (m_pEdit);
m_pSpinButtonCtrl->SetRange32( 0, 100);
m_pSpinButtonCtrl->MoveWindow ( x + cx – 16, y, 16,cy,FALSE );
m_pSpinButtonCtrl->ShowWindow(SW_SHOW);
}
也许OnEnterCell事件可以替代OnClick,但笔者发现用OnEnterCell实现起来会有一个问题:必须快速的点击,否则编辑框出现之后马上消失。所以笔者使用OnClick事件,该事件是在鼠标Up的时候才响应的。
三、 若干问题的出现及解决方案
通过以上的操作,我们可以在表格中点击某一个单元格进行编辑了,似乎我们已经实现了一个可编辑表格的制作。但在随后的测试过程中发现了如下讨厌的问题:
a在当前某个单元格处于可编辑状态而我们试图改变列宽时,发现在单元格上的编辑控件的大小并没有改变。如下
b在当前某个单元格处于可编辑状态而我们试图移动滚动条时,发现在单元格上的编辑控件的光标随之移动到了别的单元格。而且更加严重的是若点击前单元格部分显示,点击后再移动滚动条发现不只是光标移动还有控件本身也移动到了别的单元格中。如下
c在点击上下控件时会触发垂直滚动条的移动,还间接导致b问题的发生。
1、 a问题的解决
首先想到的是在MSDN中寻找MSFLEXGRID的列宽改变的响应事件,很失望没找到。但发现可以采取以下措施:在CEditGridView中的PreTranslateMessage消息响应函数中捕捉鼠标左键是按下状态并且鼠标移动的消息,在这种状态下若发现编辑控件是显示的,就调用CCtrlEditGrid的Onleave函数(开放为PUBLIC)。这样虽然在改变列宽时原来的编辑状态变为了非编辑状态,但避免了显示上不同步改变大小的问题。
BOOL CEditGridView::PreTranslateMessage(MSG* pMsg)
{
if ( ( pMsg->message == WM_MOUSEMOVE ) && ( pMsg->wParam & MK_LBUTTON ) )
{
CWnd* pWnd = FromHandle ( pMsg->hwnd );
if ( pWnd->GetRuntimeClass ( )->IsDerivedFrom ( RUNTIME_CLASS ( CMSFlexGrid ) ) )
{
if ( m_pCtrlEditGrid->m_pEdit->IsWindowVisible() )
m_pCtrlEditGrid->OnLeaveCell();
}
}
return CView::PreTranslateMessage(pMsg);
}
2、 b问题的解决
首先想到的依然是在MSDN中寻找MSFLEXGRID的列宽改变的响应事件,很幸运找到了。
在CCtrlEditGrid的头文件中添加 afx_msg void OnScroll();
在CCtrlEditGrid的CPP文件的事件映射表中添加ON_EVENT_REFLECT(CTaskEditGrid, 73 /* Scroll */, OnScroll, VTS_NONE),
编写OnScroll函数体:若单元格处于编辑状态调用Onleave函数使得编辑控件不可见。
void CCtrlEditGrid::OnScroll()
{
if ( !m_pEdit->IsWindowVisible ( ) ) return;
else
{
OnLeaveCell();
}
}
3、c问题的解决
该问题的关键是上下控件的VSCROLL事件干扰了FlexGrid的滚动事件响应,只要杜绝上下控件的VSCROLL消息不就行了吗。我们只要改造CSpinButtonCtrl的鼠标左键的响应函数就可以阻止VSCROLL的产生。
首先继承CSpinButtonCtrl产生新类CMySpinButtonCtrl,把原来在CCtrlEditGrid中的成员变量m_pSpinButtonCtrl的类型改为CMySpinButtonCtrl,动态分配时也改为CMySpinButtonCtrl类型。
接着添加CMySpinButtonCtrl的消息响应函数OnLButtonDown,注释掉该函数中默认的一行代码CSpinButtonCtrl::OnLButtonDown(nFlags, point),这样就阻止了VSCROLL消息的传递。
然后判断若点击点在Spin控件的上半部,Edit控件的值就加1,若点击在下半部,Edit控件的值就减1。这样就模拟了原来Spin控件的微调功能。
void CMySpinButtonCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
CString strNum;
int nNum;
CMySpinButtonCtrl::GetBuddy()->GetWindowText(strNum);
sscanf ( strNum, "%d", &nNum );
CRect Rect;
CMySpinButtonCtrl::GetWindowRect (&Rect);
int nLow,nUpper;
CMySpinButtonCtrl::GetRange(nLow, nUpper);
if(point.y <((Rect.bottom-Rect.top)/2))
{
nNum ++;
if ( nNum > nUpper )
nNum = nUpper;
}
else
{
nNum --;
if ( nNum < nLow )
nNum = nLow;
}
strNum.Format("%d",nNum);
CMySpinButtonCtrl::GetBuddy()->SetWindowText(strNum);
}
四、 总结
经过以上几步我们基本上完成了一个可编辑表格的制作。读者可以尝试着用本文的方法在表格中添加CComboBox,CDateTimeCtrl等不同控件。读者还可以另外增加功能,如在某一单元格处于可编辑状态时双击该单元格使之弹出一个关于本行的信息的属性表,要实现该功能可以参照a问题的解决方案,在CCtrlEditGrid的父窗口CEditGridView的PreTranslateMessage中截获双击事件,识别是否是在单元格中双击,然后弹出一个对话框,最后记住要返回True,表示该消息已被处理过不需再往下传递了。