|
|
|
|
Programmieren mit C++Visual C++ListView ControlSpalten in ListView Controls verschieben
|
Wie kann eine Spalte in einer ListView Control verschoben werden?
| Frage | |
Die ListView Control der unterschiedlichen Versionen der Common Controls besitzt keine Funktion zum Verschieben von Spalten. Um dieses Feature dennoch zu implementieren und für alle Versionen anzubieten, muß mit einer speziell erweiterten CHeaderCtrl gearbeitet werden, die zum einen das Verschieben als solches erlaubt, als auch dem Anwender ein visuelles Feedback gibt und am Ende der Spalten über Methoden der Klasse CListCtrl das Umstellen der Liste veranlaßt.
Die folgende Lösung leitet eine Klasse CMyHeaderCtrl von CHeaderCtrl ab. Der Non-Default-Konstruktor übernimmt einen Zeiger auf eine von CListCtrl oder CListView abgeleitete Klasse sowie einen Zeiger auf eine Memberfunktion, die aufzurufen ist, wenn der Anwender das Ziehen der Spalte beendet hat. Zusätzlich ist eine Funktion SetCallback() definiert, die verwendet werden kann, wenn der Default-Konstruktor der Klasse CMyListCtrl verwendet werden soll. Einige Member im Abschnitt protected dienen der Ablaufsteuerung. Sie sollen hier zunächst vorgestellt werden.
| Lösung |
BOOL m_bCheckForDrag;
Das Member m_bCheckForDrag wird vom Handler der Windows-Botschaft WM_LBUTTONDOWN auf TRUE gesetzt, wenn der Anwender die linke Maustaste über einem Spaltenkopf drückt. Verwendet wird das Flag im Handler der Botschaft WM_MOUSEMOVE, um zu entscheiden, ob auf eine Drag-Situation geprüft werden soll. Dies ist insofern wichtig, als eine Spalte nur dann gezogen werden soll, wenn der Anwender anfangs den Mausbutton über einem Spaltenkopf gedrückt hatte.
| Member m_bCheckForDrag |
BOOL m_bDragging;
Das Flag m_bDragging zeigt an, ob aktuell ein Drag-Vorgang aktiv ist (TRUE) oder nicht (FALSE).
| Member m_bDragging |
int * m_pWidth;
m_pWidth speichert ein Array mit den Breitenangaben der Spalten. Unter Rückgriff auf diese Werte kann die Spalte ermittelt werden, die das Drop-Ziel sein soll.
Das Array wird dynamisch vom Operator new allokiert.
| Member m_pWidth |
int m_nDragCol;
Das Member m_nDragCol enthält den Index der Spalte, die gezogen wird, sofern ein Dragging erfolgt.
| Member m_nDragCol |
int m_nDropPos;
In m_nDropPos steht der Index der neuen Spaltenposition.
| Member m_nDropPos |
CRect marker_rect;
Die Struktur marker_rect speichert das umhüllende Rechteck des Markers für das visuelle Feedback während des Ziehens. Hierbei handelt es sich um ein Dreieck, das die neue Position anzeigt, an die die Spalte gezogen wird. Diese Struktur wird verwendet, um den aktuellen Marker anzuzeigen wie auch den zuletzt gezeichneten Marker wieder zu löschen, wenn sich die Position ändert.
| Member marker_rect |
void (CWnd::*m_fpDragCol)(int, int);
m_fpDragCol zeigt auf eine Funktion, die aufzurufen ist, wenn der Drag-Vorgang beendet wird. In den Parametern werden der alte und neue Spaltenindex übergeben.
| Funktionszeiger m_fpDragCol |
CWnd *m_pOwnerWnd;
Das Member m_pOwnerWnd speichert eine Referenz auf ein Objekt, für das die Funktion m_fpDragCol aufgerufen wird. Normalerweise ist dies das Elternfenster. Die Steuerung des Drag-Vorgangs erfolgt im wesentlichen über die Behandlung der Windows-Botschaften WM_LBUTTONDOWN, WM_ MOUSEMOVE und WM_LBUTTONUP. OnLButtonDown() setzt den Wert von m_nDragCol und das Flag m_bCheckForDrag, wenn der Anwender den Mausbutton über einen Spaltenkopf gedrückt hatte.
| Member m_pOwnerWnd |
void CMyHeaderCtrl::OnLButtonDown(
UINT nFlags, CPoint point)
{
HD_HITTESTINFO hd_hittestinfo;
hd_hittestinfo.pt = point;
SendMessage(HDM_HITTEST, 0,
(LPARAM)(&hd_hittestinfo));
if( hd_hittestinfo.flags == HHT_ONHEADER )
{
m_nDragCol = hd_hittestinfo.iItem;
m_bCheckForDrag = TRUE;
}
CHeaderCtrl::OnLButtonDown(nFlags, point);
}
Die Methode OnMouseMove() ist für den visuellen Effekt während des Ziehens der Spalte zuständig. Sie prüft zunächst, ob der linke Mausbutton gedrückt ist und aktualisiert gegebenenfalls die Flags m_bCheckForDrag und m_bDragging.
| Methode OnLButtonDown |
void CMyHeaderCtrl::OnMouseMove(
UINT nFlags, CPoint point)
{
if( (MK_LBUTTON & nFlags) == 0)
{
m_bCheckForDrag = FALSE;
m_bDragging = FALSE;
}
else if( m_bDragging )
{
Ist ein Drag-Vorgang aktiv, wird der Wert des Members m_nDropPos aktualisiert. Dazu muß der Index der Spalte unter dem Mauszeiger ermittelt werden.
int i=0, cx = 0;
if( point.x > 0 )
for( i = 0; i < GetItemCount(); i++ )
{
if( point.x >= cx &&
point.x < cx + m_pWidth[i] )
break;
cx += m_pWidth[i];
}
Ist die aktuelle Spalte nicht mit der vorherigen identisch, muß der Bereich des alten Markers neu gezeichnet werden.
| Methode OnMouseMove |
if( i != m_nDropPos )
{
m_nDropPos = i;
CRect rect;
GetWindowRect( &rect );
InvalidateRect( &marker_rect );
Anschließend kann die neue Markierung gezeichnet werden, wobei hier ein Polygon zum Einsatz kommt.
| Marker löschen |
CClientDC dc(this);
POINT pts[3];
pts[0].x = cx;
pts[1].x = cx -3;
pts[2].x = cx +3;
pts[0].y = rect.Height();
pts[1].y = pts[2].y = rect.Height() -7;
dc.Polygon( pts, 3 );
Die neue Position des Markers wird nach marker_rect übernommen und die Funktion verlassen.
| Marker neu zeichnen |
marker_rect.left = cx - 4;
marker_rect.top = rect.Height() -8;
marker_rect.right = cx + 4;
marker_rect.bottom = rect.Height();
}
return;
}
else if( m_bCheckForDrag )
{
Liegt kein Drag-Vorgang an, ist abschließend zu prüfen, ob ein neuer Vorgang initiiert werden muß. Dies ist dann der Fall, wenn der linke Mausbutton über einem Spaltenkopf gedrückt wurde und die Maus sich nun bewegt hat.
m_bCheckForDrag = FALSE;
m_bDragging = TRUE;
m_nDropPos = m_nDragCol;
SetCapture();
Die Informationen über die Spaltenbreiten werden für den späteren Gebrauch gespeichert.
| Neue Position speichern |
int iCount = GetItemCount();
HD_ITEM hd_item;
m_pWidth = new int[iCount];
for( int i = 0; i < iCount; i++ )
{
hd_item.mask = HDI_WIDTH;
GetItem( i, &hd_item );
m_pWidth[i] = hd_item.cxy;
}
return;
}
Abschließend wird die geerbte Methode aufgerufen.
CHeaderCtrl::OnMouseMove(nFlags, point);
}
Die Methode OnLButtonUp() beendet den Ziehvorgang, sofern einer aktiv war, und restauriert zunächst die Anzeige, nachdem die Spaltenbreiten und das mauscapturing freigegeben wurden.
| Spaltenbreiten ermitteln |
void CMyHeaderCtrl::OnLButtonUp(
UINT nFlags, CPoint point)
{
ASSERT( m_pOwnerWnd != NULL &&
m_fpDragCol != NULL );
if( m_bDragging )
{
m_bDragging = FALSE;
delete[] m_pWidth;
ReleaseCapture();
Invalidate();
Anschließend werden die Callback-Funktion sowie die geerbte Methode aufgerufen.
if( m_nDragCol != m_nDropPos &&
m_nDragCol != m_nDropPos -1 )
(m_pOwnerWnd->*m_fpDragCol)( m_nDragCol,
m_nDropPos );
}
CHeaderCtrl::OnLButtonUp(nFlags, point);
}
Eine Membervariable der so abgeleiteten Klasse CMyHeaderCtrl wird in der von CListCtrl bzw. CListView abgeleiteten Klasse eingefügt...
CMyHeaderCtrl m_headerctrl;
...und über den Aufruf der Methode SetCallback() aus der abgeleiteten Klasse CListCtrl heraus initialisiert.
m_headerctrl.SetCallback(
this,
(void (CWnd::*)(int, int))DragColumn );
DragColumn zeigt dabei auf eine Callback-Funktion, die nachfolgend beschrieben wird und das Umarrangieren der Spalten leisten muß.
| Methode OnLButtonUp | |
Das Objekt CMyHeaderCtrl benötigt einen Zeiger auf eine Funktion, die aufgerufen wird, wenn der Anwender den Drag-Vorgang beendet. Als Reaktion muß diese Callback-Funktion sämtliche Arbeiten ausführen, die zum Umarrangieren der Spalten notwendig sind. Damit wird insbesondere die zugrundeliegende Klasse von der Aufgabe entlastet, die Daten korrekt handhaben zu müssen. Durch den Aufruf einer Callback-Funktion wird das gesamte Handling auf einer anderen Ebene erledigt. Nachfolgend wird eine mögliche Implementation der Callback-Funktion vorgestellt.
Die grundlegende Vorgehensweise der Methode DragColumn() besteht darin, eine neue Spalte einzufügen, alle Einträge von der Ausgangsspalte zur neu eingefügten Spalte zu kopieren und dann die Ausgangsspalte zu löschen.
In source und dest übergibt die Methode DragColumn()die Spaltenindizes der Ausgangs- und Zielspalte.
| Callback-Funktion |
void CMyListCtrl::DragColumn(int source,
int dest)
{
TCHAR sColText[160];
Als erstes ist die Zielspalte in der Control einzufügen.
LV_COLUMN lv_col;
lv_col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
lv_col.pszText = sColText;
lv_col.cchTextMax = 159;
GetColumn( source, &lv_col );
lv_col.iSubItem = dest;
InsertColumn( dest, &lv_col );
Durch diesen Einfügevorgang kann der Spaltenindex der Ausgangsspalte geändert worden sein, wenn der Index der Ausgangsspalte größer ist als der der Zielspalte, was bedeutet, daß die Spalte nach links verschoben wird. In diesem Fall ist der Index der Ausgangsspalte zu korrigieren:
if( source > dest )
source++;
Das Verschieben der Spalte an die Position 0 ist ein Spezialfall, der entsprechend abgefangen wird. Nötig wird dieser Zusatzaufwand, da die ListView Control beim Löschen und Einfügen von Spalten an der Position 0 Ausnahmen realisiert. Soll eine neue Spalte an Position 0 eingefügt werden, wenn die Control bereits mindestens eine Spalte besitzt, wird die neue Spalte tatsächlich als zweite Spalte eingefügt.
Wird die erste Spalte gelöscht, führt dies dazu, daß die Spaltenköpfe um eine Position nach links geshiftet werden und die letzte Spalte gelöscht wird. Die Methode DragColumn() umgeht diese Ausnahmesituationen durch spezielle Maßnahmen zunächst beim Einfügen.
if( dest == 0 )
for(int i = GetItemCount()-1; i > -1; i--)
SetItemText(i, 1, GetItemText( i, 0) );
Dann werden die Untereinträge der Spalte von source nach dest kopiert, indem sie per GetItemText() abgefragt und mittels SetItemText() gesetzt werden.
for( int i = GetItemCount()-1; i > -1; i--)
SetItemText(i, dest, GetItemText(i,source) );
Jetzt kann die Ausgangsspalte gelöscht werden, sofern es sich nicht um die erste Spalte mit Index 0 handelt.
if( source != 0 )
DeleteColumn( source );
else {
Ist die Ausgangsspalte 0, wurde die neue Spalte von der ListView Control an Indexposition 1 eingefügt, so daß jetzt die Einträge von Spalte 1 nach Spalte 0 kopiert werden müssen, bevor die Spalte 1 gelöscht werden kann.
GetColumn( 1, &lv_col );
lv_col.iSubItem = 0;
SetColumn( 0, &lv_col );
for (int i = GetItemCount()-1; i > -1; i--)
SetItemText(i, 0, GetItemText( i, 1) );
DeleteColumn( 1 );
}
Invalidate();
}
| Methode DragColumn | |
Zu guter Letzt muß noch ein Subclassing der Header Control erfolgen. Dies kann am besten in der Methode PreSubclassWindow() der von CListViewCtrl abgeleiteten Klasse durchgeführt werden. Wird hingegen eine Ableitung von CListView verwendet, ist der Code für das Subclassing in OnInitialUpdate() am besten aufgehoben. In beiden Fällen ist sicherzustellen, daß die jeweilige Methode der Basisklasse aufgerufen wird, bevor das Subclassing erfolgt.
void CMyListCtrl::PreSubclassWindow()
{
CListCtrl::PreSubclassWindow();
m_headerctrl.SubclassWindow( ::GetDlgItem(m_hWnd,0) );
}
Wurde die ListView Control nicht im Reportmodus erzeugt, muß der Stil der ListView Control entsprechend geändert werden, bevor ein Subclassing der Header Control erfolgen kann. Hierzu kann die Methode ModifyStyle() verwendet werden.
Der Grund für die Änderung des Stils auf den Reportmodus ist die Tatsache, daß die Header Control genau dann erzeugt wird, wenn die ListView Control zum ersten Mal in den Reportmodus versetzt wird.
| Subclassing der Header Control
|
|
|