60 Control List Box de arrastre

Listbox de arrastre

Los listbox de arrastre son un tipo especializado de listbox que permite al usuario arrastrar ítems y cambiar sus posiciones.

Con éste capítulo completamos la documentación de los list box, al menos hasta el punto de la versión del sistema operativo en la fecha en que se escribe.

Crear un list box de arrastre

Para crear un list box de arrastre empezaremos por crear un list box normal, con los estilos que prefiramos, y después invocaremos la función MakeDragList, indicando como parámetro el manipulador de ventana del control. Esto lo convertirá en un list box de arrastre.

Las operaciones de arrastre se realizan procesando los códigos de notificación que envía el control cuando el usuario pincha sobre algún ítem y lo arrastra a otra posición.

Pero estos códigos de notificación no se envían a través de un mensaje WM_NOTIFY, como en otros controles, sino a través de un mensaje de notificación creado específicamente para estos controles.

Debemos, pues, obtener el valor de este mensaje. Esto se hace mediante la función RegisterWindowMessage, indicando como parámetro el valor DRAGLISTMSGSTRING, definido como la cadena "commctrl_DragListMsg".

Esta función registra un mensaje que puede ser usado por varias aplicaciones.

El valor retornado podrá ser usado en el procedimiento de ventana o diálogo para procesar las notificaciones de controles list box de arrastre.

    static UINT NOTIFICACION;
    HWND hctrl;

    hctrl = CreateWindowEx(0, "LISTBOX", NULL,
        WS_CHILD | WS_VISIBLE | WS_TABSTOP | LBS_STANDARD,
        80,10,160,200,
        hwnd, (HMENU)ID_DRAGLISTBOX,
        hInstance, NULL);
    MakeDragList(hctrl);
    NOTIFICACION = RegisterWindowMessage(DRAGLISTMSGSTRING);

Procesar notificaciones

Las operaciones de arrastre se notifican a la aplicación a través del mensaje que hemos registrado previamente. Al tratarse de una variable no debemos usar un 'case' del 'switch' que normalmente usamos para procesar los mensajes. Es preferible añadir una sentencia 'if' dentro de la etiqueta 'default'.

En el parámetro wParam recibiremos el identificador del control que envía la notificación y en lParam un puntero a una estructura DRAGLISTINFO que contiene información sobre la notificación: el manipulador del control, el código de notificación y la posición del ratón.

DRAGLISTINFO *lpDLI;
...
switch (msg) {
...
    default:
        if(msg == NOTIFICACION) {
            lpDLI = (DRAGLISTINFO*)lParam;
            if(lpDLI->hWnd == GetDlgItem(hwnd, ID_DRAGLISTBOX))
                switch(lpDLI->uNotification) {
...
                }
            } else
        return DefWindowProc(hwnd, msg, wParam, lParam);
}

El primer código de notificación que recibiremos será DL_BEGINDRAG, cuando el usuario haga click con el botón izquierdo sobre un ítem. Usaremos la función LBItemFromPt para obtener el índice del ítem a partir de las coordenadas del ratón que recibimos en el miembro ptCursor de la estructura apuntada por lParam.

Tenemos que guardar el valor del índice que estamos arrastrando para usarlo cuando la operación de arrastre termine.

Si procesamos esta notificación y decidimos dar comienzo a una operación de arrastre, deberemos retornar TRUE. Si por el contrario, no queremos arrastrar el ítem (por la razón que sea), retornaremos FALSE.

Mientras el usuario mantenga pulsado el botón izquierdo del ratón, cuando el ratón se mueva se enviará un código de notificación DL_DRAGGING.

Lo habitual cuando se procesa este código, es mostrar un icono que indica la posición donde se insertaría el ítem si se terminara el arrastre en ese momento. Para ello, lo primero que necesitamos es averiaguar el índice del ítem sobre el que está el ratón. De nuevo usaremos la función LBItemFromPt para ello.

Para mostrar el icono de inserción usaremos la función DrawInsert, indicando en el primer parámetro el manipulador de la ventana padre del control, en el segundo el manipulador de ventana del control list box, y en el tercero el índice del ítem.

Cuando se procesa este código de notificación, el valor de retorno indicará qué cursor mostrar durante el arrastre. Este valor de retorno puede ser DL_STOPCURSOR que mostrará el cursor de stop que se suele usar para indicar al usuario que la posición actual no es válida, DL_COPYCURSOR que muestra el cursor de copia, que sirve para indicar que el ítem será copiado o DL_MOVECURSOR para indicar que el ítem será movido.

El cursor sólo sirve como información visual para el usuario, y el comportamiento del arrastre dependerá exclusivamente del código encargado de procesar las notificaciones de arrastre.

La operación de arrastre puede terminar de dos formas. Si el usuario pulsa la tecla ESC o pulsa el botón derecho del ratón durante el arrastre, la operación se cancela y se enviá el código de notificación DL_CANCELDRAG a la aplicación.

Esto terminará la operación de arrastre sin realizar cambios. Sin embargo, el icono de inserción que mostramos durante el arrastre no será eliminado automáticamente, ni hay una función específica para hacerlo. En su lugar podemos redibujar la ventana completa, mediante una función InvalidateRect.

Si la operación de arrastre termina porque el usuario ha liberado el botón izquierdo del ratón, se enviará el código de notificación DL_DROPPED.

Aprovecharemos este momento para realizar la tarea que hayamos elegido para esta operación.

switch(lpDLI->uNotification) {
    case DL_BEGINDRAG:
        itemDrag = LBItemFromPt(lpDLI->hWnd, lpDLI->ptCursor, FALSE);
        return TRUE;
    case DL_DRAGGING:
        if(itemDrag != -1) {
            itemPos = LBItemFromPt(lpDLI->hWnd, lpDLI->ptCursor, TRUE);
            DrawInsert(hwnd, lpDLI->hWnd, itemPos);
            return DL_MOVECURSOR;
        }
        break;
    case DL_CANCELDRAG:
        itemDrag = -1;
        InvalidateRect(hwnd, 0, TRUE);
        break;
    case DL_DROPPED:
        if(itemDrag != -1) {
            itemPos = LBItemFromPt(lpDLI->hWnd, lpDLI->ptCursor, FALSE);
            if(itemPos > itemDrag) itemPos--;
            SendMessage(lpDLI->hWnd, LB_GETTEXT, (WPARAM)itemDrag, (LPARAM)cad);
            SendMessage(lpDLI->hWnd, LB_DELETESTRING, (WPARAM)itemDrag, 0);
            SendMessage(lpDLI->hWnd, LB_INSERTSTRING, (WPARAM)itemPos, (LPARAM)cad);
            SendMessage(lpDLI->hWnd, LB_SETCURSEL, (WPARAM)itemPos, 0);
            itemDrag = -1;
            InvalidateRect(hwnd, 0, TRUE);
        }
        break;
}   

En este ejemplo usamos el valor de itemDrag para almacenar el índice del ítem que se está arrastrando. Además si ese valor es -1 indicará que no hay una operación de arrastre en curso.

Cuando se termina de arrastrar un ítem, en este ejemplo, se insertará en la nueva posición. Para ello primero obtenemos el texto del ítem a mover, mediante un mensaje LB_GETTEXT, a continuación lo eliminamos del list box, usando un mensaje LB_DELETESTRING y volvemos a insertarlo en la nueva posición, con un mensaje LB_INSERTSTRING.

Pero tenemos que tener cuidado con un detalle. Si la nueva posición del ítem está más abajo que la anterior (el identificador de la nueva posición es mayor que el de la original), al borrar el ítem, todas las posiciones a continuación del eliminado tendrán un valor una unidad menor. Por eso, en ese caso, decrementamos el valor de itemPos.

Por último, seleccionamos el ítem en la nueva posición, usando el mensaje LB_SETCURSEL, para asegurarnos de que es visible en el control list box.

Ejemplo 102

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 102 win102.zip 2021-12-11 7628 bytes 656