2005年04月21日 swimmingfish2004
一、 概述
在我們製作的用戶界面中很多時候會用到表格,當然我們首先想到的是用控件,如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,表示該消息已被處理過不需再往下傳遞了。