|
|
|
|
Programmieren mit C++Visual C++ListView ControlUntereinträge editieren
|
Was ist zu tun, damit der Anwender Untereinträge einer ListView Control editieren kann?
| Frage | |
Per Default erlaubt die ListView Control lediglich das Editieren der ersten Spalte, wenn der Stil LVS_EDITLABELS gesetzt ist. Damit auch Untereinträge editierbar werden, muß eine eigene Edit Control erzeugt werden, die dem Eintrag zugeordnet wird.
Zunächst ist eine Klasse von CListCtrl abzuleiten. Allerdings kann auch eine Ableitung von CListView erfolgen, wenn die Funktionalität einer CView anstelle einer Control benötigt wird. Anschließend sind in der abgeleiteten Klasse einige Erweiterungen vorzunehmen.
Benötigt wird zunächst eine Funktion zum Erkennen der Spalte, in der ein Eintrag angeklickt wurde. Dies kann über die in Kapitel 5/2.6.2.1 vorgestellte Funktion HitTestEx() erfolgen.
| Lösung | |
Als nächstes ist eine Funktion hinzuzufügen, die das Editieren initiiert. Hier sind drei mögliche Anwenderaktionen zu berücksichtigen:
- Klick auf eine bereits selektierte Zeile
- Doppelklick auf ein Element
- oder sogar ein Push Button
| Anwenderaktionen | |
Die Hilfsfunktion EditSubLabel() übernimmt die Zeile und Spalte des Untereintrags, der editiert werden soll. Dazu stellt die Funktion zunächst sicher, daß der entsprechende Eintrag sichtbar ist, bevor eine Edit Control erzeugt wird, deren Größe und Position auf die Größe des zu editierenden Eintrags der ListView Control abgestellt ist, ebenso wie auch die Textausrichtung der Edit Control, die der Ausrichtung des ListView-Elements angepaßt wird.
Die neu erzeugte Edit Control ist vom Klassentyp CInPlaceEdit, der weiter unten beschrieben wird.
Als Funktionsergebnis returniert die Funktion EditSubLabel() einen temporären Zeiger auf die neu erzeugte Edit Control.
| Initialisierung des Editvorgangs |
CEdit* CMyListCtrl::EditSubLabel(int nItem,
int nCol)
{
if (!EnsureVisible(nItem, TRUE))
return NULL;
Zunächst ist sicherzustellen, daß die übergebene Spalte, die zuvor vom Aufrufer ermittelt wurde, gültig ist.
CHeaderCtrl* pHeader = (CHeaderCtrl*)GetDlgItem(0);
int nColumnCount = pHeader->GetItemCount();
if (nCol >= nColumnCount || GetColumnWidth(nCol) < 5 )
return NULL;
Der Offset der Spalte ergibt sich durch Summieren der einzelnen Spaltenbreiten, bis die betroffene Spalte als nächste ansteht.
int offset = 0;
for( int i = 0; i < nCol; i++ )
offset += GetColumnWidth( i );
CRect rect;
GetItemRect( nItem, &rect, LVIR_BOUNDS );
Mit Hilfe des Spaltenoffsets und dem bekannten Zeilenindex kann jetzt sichergestellt werden, daß der zu editierende Eintrag sichtbar ist, indem die ListView entsprechend gescrollt wird, sofern dies notwendig sein sollte.
| Methode EditSubLabel |
CRect rcClient;
GetClientRect( &rcClient );
if( offset + rect.left < 0 || offset +
rect.left > rcClient.right )
{
CSize size;
size.cx = offset + rect.left;
size.cy = 0;
Scroll( size );
rect.left -= size.cx;
}
Die Textausrichtung des betroffenen Eintrags der ListView Control läßt sich wie folgt ermitteln:
LV_COLUMN lvcol;
lvcol.mask = LVCF_FMT;
GetColumn( nCol, &lvcol );
DWORD dwStyle ;
if((lvcol.fmt&LVCFMT_JUSTIFYMASK) == LVCFMT_LEFT)
dwStyle = ES_LEFT;
else if((lvcol.fmt&LVCFMT_JUSTIFYMASK) == LVCFMT_RIGHT)
dwStyle = ES_RIGHT;
else dwStyle = ES_CENTER;
Das umhüllende Rechteck des Eintrags ergibt sich anhand der Spaltenbreite gemäß:
rect.left += offset+4;
rect.right = rect.left + GetColumnWidth( nCol ) - 3 ;
if( rect.right > rcClient.right)
rect.right = rcClient.right;
Nachdem alle notwendigen Eigenschaften bekannt sind, kann die Edit Control erzeugt werden.
dwStyle |= WS_BORDER|WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL;
CEdit *pEdit = new CInPlaceEdit(nItem, nCol,
GetItemText( nItem, nCol ));
pEdit->Create( dwStyle, rect, this,
IDC_IPEDIT );
return pEdit;
}
Der von der Methode EditSubLabel() gelieferte Zeiger muß nicht gespeichert werden, da die Klasse CInPlaceEdit so konzipiert ist, daß sie die Edit Control zerstört und das Objekt freigibt, wenn sie den Fokus verliert.
Ein Klick auf die Laufleisten der ListView Control führt nicht zum Verlust des Fokus der Edit Control. Aus diesem Grund müssen die Scroll-Nachrichten bearbeitet werden, um den Fokus von der Edit Control auf die ListView Control umzusetzen.
void CMyListCtrl::OnHScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar)
{
if( GetFocus() != this )
SetFocus();
CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
}
void CMyListCtrl::OnVScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar)
{
if( GetFocus() != this )
SetFocus();
CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar);
}
Analog zur implementierten Edit-Routine für die erste Spalte sendet auch die CInPlaceEdit Control eine LVN_ENDLABELEDIT-Nachricht, wenn der Editiervorgang beendet ist. Ist für das Behandeln dieser Nachricht noch kein Handler implementiert, muß dies nachgeholt werden.
Eine mögliche Implementation wäre die folgende:
void CMyListCtrl::OnEndLabelEdit(NMHDR* pNMHDR, LRESULT* pResult)
{
LV_DISPINFO *plvDispInfo = (LV_DISPINFO *)pNMHDR;
LV_ITEM *plvItem = &plvDispInfo->item;
Ist der Text nicht NULL, wird der Text in die ListView Control übernommen.
if (plvItem->pszText != NULL)
{
SetItemText(plvItem->iItem,
plvItem->iSubItem,
plvItem->pszText);
}
*pResult = FALSE;
}
Damit die Edit Control überhaupt erzeugt wird, muß ein entsprechendes Interface implementiert werden. Naheliegenderweise erfolgt dies über das OnLButtonDown, da ein Klick oder ein Doppelklick auf den Untereintrag das Editieren in Gang setzt. Der Handler für die WM_LBUTTONDOWN-Nachricht muß insbesondere den Stil LVS_EDITLABELS verifizieren, bevor die Edit Control erzeugt wird. Des weiteren soll keine Edit Control für Aktionen in der ersten Spalte erzeugt werden, da dies über die ListView Control selbst gehandelt werden kann.
| Eintrag sichtbar machen |
void CMyListCtrl::OnLButtonDown(UINT nFlags,
CPoint point)
{
int index;
CListCtrl::OnLButtonDown(nFlags, point);
Test auf Spaltenindex und Stil der ListView Control:
int colnum;
if ((index = HitTestEx( point, &colnum ))!=-1)
{
UINT flag = LVIS_FOCUSED;
if( (GetItemState( index, flag ) & flag) == flag && colnum > 0)
{
if( GetWindowLong(m_hWnd, GWL_STYLE) & LVS_EDITLABELS )
EditSubLabel( index, colnum );
}
else SetItemState(index, LVIS_SELECTED | LVIS_FOCUSED ,
LVIS_SELECTED | LVIS_FOCUSED);
}
}
Damit die Edit Control alle notwendigen Aktionen selbständig ausführen kann, muß ein Subclassing der CEdit Class erfolgen. Im wesentlichen soll die neue Klasse CInPlaceEdit
- eine LVN_ENDLABELEDIT senden, wenn der Editiervorgang beendet ist,
- die Control expandieren, um den Text aufzunehmen
- sich selbst zerstören, wenn das Editieren beendet ist
- das Editieren beenden, wenn der Anwender die Enter- oder Tabulator-Taste betätigt bzw. die Edit Control den Fokus verliert.
Der Konstruktor der Klasse CInPlaceEdit übernimmt lediglich die Argumente und initialisiert die internen Member. Von besonderem Interesse ist dabei das Member m_bESC, das mit FALSE initialisiert wird. m_bESC zeigt an, ob der Editiervorgang abgebrochen wurde oder nicht. Die überschriebene Methode PreTranslateMessage() hat sicherzustellen, daß spezielle Tastaturereignisse an die Edit Control durchgereicht werden. Die Escape- und Enter-Tasten werden normalerweise vom CDialog- oder CFormView-Objekt in PreTranslateMessage() behandelt. Die Klasse muß daher auf diese Eingaben testen und die Ereignisse ggf. an die Edit Control weiterleiten. Des weiteren muß auf GetKeyState(VK_CONTROL) geprüft werden, damit Strg-C, Str-V und Strg-X Shortcuts durch die Edit Control verarbeitet werden.
| Methode OnLButtonDown |
BOOL CInPlaceEdit::PreTranslateMessage(
MSG* pMsg)
{
if( pMsg->message == WM_KEYDOWN )
{
if (pMsg->wParam == VK_RETURN
|| pMsg->wParam == VK_DELETE
|| pMsg->wParam == VK_ESCAPE
|| GetKeyState( VK_CONTROL))
{
::TranslateMessage(pMsg);
::DispatchMessage(pMsg);
return TRUE;
}
}
return CEdit::PreTranslateMessage(pMsg);
}
Der Handler OnKillFocus() versendet die Nachricht LVN_ENDLABELEDIT und zerstört die Edit Control. Dabei ist zu beachten, daß die Nachricht an das Elternfenster der ListView Control und nicht an die ListView Control selbst gesendet wird. Über das Abbruchflag m_bESC wird dabei gesteuert, ob ein Text oder ein NULL-String zu übergeben ist.
void CInPlaceEdit::OnKillFocus(CWnd* pNewWnd)
{
CEdit::OnKillFocus(pNewWnd);
CString str;
GetWindowText(str);
LV_DISPINFO dispinfo;
dispinfo.hdr.hwndFrom = GetParent()->m_hWnd;
dispinfo.hdr.idFrom = GetDlgCtrlID();
dispinfo.hdr.code = LVN_ENDLABELEDIT;
dispinfo.item.mask = LVIF_TEXT;
dispinfo.item.iItem = m_iItem;
dispinfo.item.iSubItem = m_iSubItem;
dispinfo.item.pszText = m_bESC ? NULL : LPTSTR((LPCTSTR)str);
dispinfo.item.cchTextMax = str.GetLength();
GetParent()->GetParent()->SendMessage(
WM_NOTIFY, GetParent()->GetDlgCtrlID(),(LPARAM)&dispinfo );
DestroyWindow();
}
Der passende Platz zum Zerstören des C++-Objekts ist die Methode OnNcDestroy().
| Methode PreTranslate-Message |
void CInPlaceEdit::OnNcDestroy()
{
CEdit::OnNcDestroy();
delete this;
}
Der OnCreate() Handler erzeugt die Edit Control und initialisiert sie mit den passenden Werten.
| Methode OnNcDestroy |
int CInPlaceEdit::OnCreate(
LPCREATESTRUCT lpCreateStruct)
{
if (CEdit::OnCreate(lpCreateStruct) == -1)
return -1;
CFont* font = GetParent()->GetFont();
SetFont(font);
SetWindowText( m_sInitText );
SetFocus();
SetSel( 0, -1 );
return 0;
}
| Methode OnCreate | |
Die Funktion OnChar() beendet den Editiervorgang beim Auftreten der Tasten Escape oder Enter, indem beim Auftreten dieser Zeichen der Fokus auf die ListView Control gesetzt wird, was dazu führt, daß die Methode OnKillFocus() ausgeführt wird, was zur Zerstörung der Klasse führt.
void CInPlaceEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if( nChar == VK_ESCAPE ||
nChar == VK_RETURN)
{
if( nChar == VK_ESCAPE )
m_bESC = TRUE;
GetParent()->SetFocus();
return;
}
Alle übrigen Zeichen läßt die Methode OnChar() durch die ererbte Methode bearbeiten, bevor sie testet, ob die Größe der Control angepaßt werden muß.
CEdit::OnChar(nChar, nRepCnt, nFlags);
Falls erforderlich, versucht die Funktion, die Größe der Control anzupassen. Dazu ermittelt die Funktion zunächst die Länge des neuen Strings in der korrekten Schriftart...
| Zeichenbearbeitung |
CString str;
GetWindowText( str );
CWindowDC dc(this);
CFont *pFont = GetParent()->GetFont();
CFont *pFontDC = dc.SelectObject( pFont );
CSize size = dc.GetTextExtent( str );
dc.SelectObject( pFontDC );
size.cx += 5; // Extrabuffer
...und vergleicht die Länge mit der aktuellen Größe der Control, die sich wie folgt ergibt:
CRect rect, parentrect;
GetClientRect( &rect );
GetParent()->GetClientRect( &parentrect );
Die Transformation auf Elternkoordinaten ergibt das tatsächliche Rechteck, das beim Vergrößern der Control direkt zugewiesen werden kann.
| Länge des Strings |
ClientToScreen( &rect );
GetParent()->ScreenToClient( &rect );
Ist der String zu lang, um in die Edit Control zu passen, wird versucht, die Edit Control zu vergrößern, nachdem das Elternfenster, also die ListView Control, darauf getestet wurde, ob ausreichend Platz für das Wachsen der Control vorhanden ist.
| Koordinatentransformation |
if( size.cx > rect.Width() )
{
if(size.cx + rect.left < parentrect.right)
rect.right = rect.left + size.cx;
else rect.right = parentrect.right;
MoveWindow( &rect );
}
}
| Größe vergleichen und anpassen
|
|
|