- Cuadros de diálogo de uso común
- Uso de SHBrowseForFolder()
- Lista de identificadores
- Funcionamiento básico
- La función de procesamiento de mensajes
- Planificación del componente
- Definición previa de tipos
- Definición de clases
- Implementación del componente
- Implementación de los editores
- Probando el componente TSelectFolderDialog
Delphi incorpora múltiples componentes que permiten usar los cuadros de diálogos comunes del sistema, pero ninguno que permita elegir una carpeta. En este tutorial te enseño cómo crearlo.
Una de las posibilidades más interesantes de Delphi, como todos sabemos, es que podemos crear nuestros propios componentes e integrarlos en el entorno. Esta tarea, además, no requiere demasiado trabajo, puesto que contamos con un asistente que crea un esqueleto genérico, a completar por nosotros, y otro que se encarga de incluir el componente en un paquete, compilarlo e instalarlo.
Desarrollar componentes VCL, por tanto, no es un objetivo fuera del alcance de cualquier programador que use Delphi. Realmente, basta con echarle un poco de imaginación o, en algunos casos, tener una necesidad concreta. En este tutorial podrás encontrar el desarrollo completo de un componente que encapsula una funcionalidad de Windows. Lo más interesante del componente es, lógicamente, su código fuente. Asumiendo que conocemos las bases necesarias para crear componentes, vamos a centrarnos en el diseño concreto del componente que nos ocupe, sin indicar cómo se inicia la creación del componente, cómo se aloja en un paquete o cómo se instala.
Cuadros de diálogo de uso común
La mayor parte de las aplicaciones, independientemente de su finalidad concreta, cuentan con necesidades más o menos comunes: abrir archivos o guardarlos, seleccionar tipos de letra, imprimir información, etc. Tan comunes son estas necesidades, que el propio sistema operativo aporta una solución común: unos cuadros de diálogo que, como no podía ser de otra manera, se apellidan de uso común. Los componentes que encontramos en la página Dialogs de la paleta de Delphi (véase Figura 1), lo que hacen es simplificar para nosotros, los programadores que utilizamos Delphi, la utilización de esos servicios del sistema.
Utilizar uno de los componentes de la citada página, es algo que se reduce a establecer algunas propiedades, generalmente durante la fase de diseño, y llamar posteriormente, en ejecución, al método Execute()
, que hace aparecer el cuadro de diálogo. Cuando éste es cerrado, el programa puede recuperar la selección del usuario leyendo algunas propiedades.
Una acción que en ocasiones también puede ser precisa, y que no está disponible a través de ninguno de los componentes de Delphi, es la selección de una carpeta. Suponga que su programa tiene que preguntar al usuario dónde quiere alojar sus bases de datos, dándole a elegir entre las carpetas disponibles en el sistema. ¿Qué componente sería el adecuado para esta tarea? Componentes como TOpenDialog
y TSaveDialog
están orientados a la selección de archivos, no de carpetas.
Windows cuenta con una función, llamada SHBrowseForFolder()
, que hace precisamente lo que podemos necesitar en esas ocasiones. Dicha función abre un cuadro de diálogo como el mostrado en la Figura 2, permitiendo al usuario seleccionar cualquiera de las carpetas existentes en el sistema. Para poder utilizar esta función, deberá incluirse el módulo ShlObj
en nuestro proyecto.
Tener que utilizar la función SHBrowseForFolder()
cada vez que se precisa facilitar la selección de una carpeta, no es, precisamente, nada cómodo. Como veremos en un momento, es preciso preparar una estructura de parámetros, escribir una función callback para procesar mensajes, convertir identificadores de Windows en caminos que podamos usar en el programa, etc. Se trata, por tanto, de una candidata perfecta para ser encapsulada en forma de componente.
SHBrowseForFolder()
Uso de Para poder crear un componente que facilite el uso de esta función deberemos, lógicamente, conocer los parámetros que necesita, los valores que devuelve y, en general, su funcionamiento global. Vamos a dedicar éste y los puntos siguientes a estudiar detalladamente cómo se usa SHBrowseForFolder()
, en una descripción básicamente teórica. Después, con las ideas claras acerca de lo que pretendemos, nos pondremos manos a la obra con el desarrollo del componente.
Antes de poder llamar a esta función, deberemos declarar una variable de tipo TBrowseInfo
, un registro, asignando los valores apropiados a cada uno de sus miembros. Algunos de estos valores, como el título que aparecerá en la ventana, son obvios. Otros, por el contrario, no lo son tanto y requieren una mayor explicación. En la tabla siguiente se enumeran los miembros del mencionado registro, indicando el tipo y contenido de cada uno de ellos.
Nombre del miembro | Comentario |
hwndOwner |
Identificador de la ventana que actuará como padre del cuadro de diálogo |
pidlRoot |
Lista de identificadores de la carpeta de la que partirá la selección |
pszDisplayName |
Cadena para recuperar el nombre del elemento seleccionado |
lpszTitle |
Cadena con el título que aparecerá en la ventana |
ulFlags |
Indicadores con opciones de funcionamiento |
lpfn |
Puntero a la función callback de proceso de mensajes |
lParam |
Parámetro de usuario, que será enviado a la función anterior |
iImage |
Índice de la imagen que representa al elemento seleccionado |
Los miembros hwndOwner
, pszDisplayName
, lpszTitle
y lParam
no requieren demasiada explicación. El primero es el identificador de la ventana que actuará como padre del cuadro de diálogo, el segundo apuntará a una cadena en la que se almacenará el nombre del elemento que se haya elegido, el tercero será el mencionado título de la ventana y el último, un Integer
, es un parámetro de usuario, útil tan sólo si se utiliza una función callback.
El miembro lpfn
puede ser nulo, caso en el cual el funcionamiento del cuadro de diálogo será autónomo. Si se entrega la dirección de una función, por el contrario, el cuadro de diálogo notificará diversos eventos, como la inicialización o el cambio de selección. En un punto posterior entraremos en mayor detalle sobre la función callback, los mensajes que podemos recibir y los que es posible enviar al cuadro de diálogo.
Por defecto, el cuadro de diálogo mostrado por SHBrowseForFolder()
permite elegir no solo carpetas, sino también ordenadores, impresoras y cualquier otro elemento que exista en el espacio de nombres de la interfaz de Windows. Mediante el parámetro ulFlags
es posible especificar una o más opciones, representadas por una serie de constantes. De esta forma podríamos, por ejemplo, limitar la selección sólo a carpetas del sistema de archivos, incluir también la visualización de archivos o incluir una línea de estado para mostrar un texto. Consulte la ayuda en línea de Delphi sobre el API de Windows para obtener una enumeración de todas las constantes posibles.
Cada uno de los elementos que aparecen en el cuadro de diálogo tiene un pequeño icono asociado, que indica si dicho elemento es una carpeta, una unidad de disco, un ordenador, etc. Todas esas imágenes están almacenadas en un componente ImageList
gestionado por el propio sistema. El miembro iImage contendrá el índice de la imagen, relativa a ese componente, que representa al objeto elegido.
Lista de identificadores
En un cuadro de diálogo de apertura de archivo sólo pueden elegirse archivos, elementos que tienen una correspondencia con el sistema de almacenamiento físico del ordenador. En el cuadro de diálogo mostrado por SHBrowseForFolder()
, sin embargo, aparecen elementos que no tienen esa correspondencia, elementos tales como impresoras u ordenadores. Al no ser elementos pertenecientes al sistema de archivos, no cuentan con un camino.
Dado que unos elementos cuentan con camino y otros no, Windows identifica a los objetos usando otro recurso alternativo, en lugar de un camino. Dicho recurso es una lista de identificadores concatenados, conocida habitualmente como PIDL (Pointer to IDentifier List, Puntero a lista de identificadores). Todos los elementos del espacio de nombres cuentan con un PIDL único, que les identifica de manera inequívoca.
Cuál es la estructura interna de un PIDL es algo que no nos interesa especialmente. Lo que sí nos interesa, es saber que la función SHBrowseForFolder()
no nos devolverá un camino indicando cuál es el elemento elegido, dado que algunos elementos no cuentan con camino. Por eso, esta función lo que devuelve es un PIDL, un puntero a una lista de identificadores de longitud variable.
¿Y qué hacemos nosotros con un PIDL? No sabemos cuál es su estructura ni qué significa, ni siquiera si es mineral, vegetal o animal. En principio, puesto que lo que buscamos es facilitar la selección de carpetas, nos interesará saber cómo obtener el camino del objeto al que representa ese PIDL. Con este fin podemos usar la función SHGetPathFromIDList()
, facilitando como primer parámetro el PIDL y como segundo una cadena con al menos MAX_PATH
bytes de espacio disponible. Si el valor de retorno de esta función es True, ello significará que el objeto tiene un camino y éste ha sido devuelto en el segundo parámetro.
La conversión en sentido inverso, obteniendo el PIDL que corresponde a un determinado camino, es algo más complejo y requiere varios pasos. Lo primero es obtener un puntero a la interfaz IShellFolder
del escritorio, simplemente llamado a la función SHGetDesktopFolder(). Acto seguido, usando dicho puntero, se llamaría al método ParseDisplayName()
. El tercer parámetro sería el camino de entrada, y el penúltimo un puntero al PIDL obtenido. De esta forma podríamos, por ejemplo, facilitar una carpeta de inicio en el miembro pidlRoot
del registro TBrowseInfo
. Si el PIDL que queremos recuperar es el de una de las carpetas especiales de Windows, como la de archivos de programa o documentos, tenemos otra función disponible: SHGetSpecialFolderLocation()
.
Los PIDL son estructuras de datos creadas por Windows a demanda nuestra, como respuesta a las llamadas de SHBrowseForFolder()
o ParseDisplayName()
. Estas estructuras se almacenan en un espacio de memoria común usado por el motor COM, siendo responsabilidad nuestra su liberación, cuando ya no nos sean útiles. Con este fin usaremos la función CoTaskMemFree()
, facilitando como único parámetro el PIDL.
Funcionamiento básico
Con lo que conocemos hasta ahora, tenemos información suficiente para hacer aparecer el cuadro de diálogo de la Figura 1, saber si se ha elegido una carpeta y, en caso afirmativo, recuperar el camino correspondiente. Antes de continuar estudiando otros detalles, veamos con un ejemplo muy simple como aplicar en la práctica las explicaciones anteriores.
Inicia un nuevo proyecto de tipo estándar, inserta en el formulario un botón y haz doble clic sobre él. Añade a la cláusula Uses
los módulos ShlObj
y Activex
, necesarios para usar funciones como SHBrowseForFolder()
o SHGetDesktopFolder()
. Por último, inserta el código listado a continuación.
El código está ampliamente comentado. Básicamente, lo que hacemos es obtener el PIDL que corresponde al camino 'C:\'
, a fin de mostrar el cuadro de diálogo sólo con las carpetas existentes en dicho camino. Tras preparar toda la estructura de datos, mostramos el cuadro de diálogo y, en caso de que el PIDL devuelto no sea nulo, obtenemos el camino correspondiente a la carpeta y lo comunicamos mediante un mensaje.
Al ejecutar el programa y pulsar sobre el botón, podrás apreciar que el cuadro de diálogo no muestra todo el espacio de nombres de Windows, sino sólo aquellos elementos que existen en el camino indicado antes. Además, al haberse incluido como opción la constante BIF_RETURNONLYFSDIRS
, si selecciona un elemento que no sea una carpeta el botón Aceptar se desactivará automáticamente.
La función de procesamiento de mensajes
Como has podido ver en el punto anterior, no es necesario codificar una función callback de proceso de mensajes para poder aprovechar la función SHBrowseForFolder()
. Dicha función, sin embargo, será precisa si deseamos aprovechar todas las posibilidades disponibles.
El cuadro de diálogo mostrado por esta función cuenta, como toda ventana, con un manejador. Éste, no obstante, sólo existe desde el momento en que aparece en pantalla, al llamar a SHBrowseForFolder()
, hasta que se cierra. Es un dato, por tanto, que no podremos obtener ni usar sin una función de proceso de mensajes. Este manejador lo podemos usar, por ejemplo, para cambiar la posición del cuadro de diálogo en pantalla, activar o desactivar el botón Aceptar que aparece en ella, etc.
Los mensajes que el cuadro de diálogo puede enviar a la función callback son tres: BFFM_INITIALIZED
, BFFM_SELCHANGED
y BFFM_VALIDATEFAILED
. El primero se producirá siempre, pero una sola vez, justo en el momento en que el cuadro de diálogo ha sido inicializado, antes de que se haga visible en pantalla. El segundo tiene lugar cada vez que se cambia de un elemento a otro en la ventana, aún sin pulsar botón alguno. El último se da pocas veces, ya que sólo es posible si se hace aparecer un campo de edición en el interior de la ventana y, además, el usuario introduce un camino inválido en él.
El mensaje BFFM_SELCHANGED
es especialmente interesante. Cada vez que recibe este mensaje, la función dispondrá también del PIDL del nuevo elemento seleccionado en ese momento. Esto permite que el programa compruebe si dicho elemento es válido o no para sus fines y, en consecuencia, active o desactive el botón Aceptar.
Desde la primera llamada a la función de proceso mensajes, nuestro componente podrá disponer del manejador del cuadro de diálogo. Podemos usarlo para enviarle mensajes a esa ventana, por ejemplo para indicar el texto de la línea de estado, seleccionar un determinado elemento, activar o desactivar el botón Aceptar. Los tres mensajes posibles son BFFM_ENABLEOK
, BFFM_SETSELECTION
y BFFM_SETSTATUSTEXT
. El parámetro asociado será, por orden, un Boolean indicando si el botón se activa o desactiva, el PIDL o camino de la nueva selección y la cadena del nuevo texto.
Planificación del componente
Conociendo la teoría de funcionamiento de SHBrowseForFolder()
y sirviéndonos de la ayuda de API que incorpora Delphi, vamos a planificar las características del componente que queremos desarrollar, tras lo cual iniciaremos su codificación. Aunque la finalidad principal, y más habitual del componente, será facilitar la selección de una carpeta del sistema de archivos, tampoco impediremos que sea utilizado para elegir un ordenador o una impresora.
Al igual que los componentes TOpenDialog
o TSaveDialog
, el nuestro, al que vamos a llamar TSelectFolderDialog
, contará con propiedades como Title
y Options
, así como con el método Execute()
, que se comportará como el homónimo de los componentes citados. Además de esas dos propiedades, necesitaremos otras para establecer la carpeta que actuará como raíz, modificar o establecer el camino actual, mostrar un texto en la línea de estado, etc. En la Tabla siguiente puede verse la lista de propiedades que implementaremos en el componente, junto con su tipo y un breve comentario.
Nombre de la propiedad | Tipo | Comentario |
Title |
String |
Título que aparecerá en la ventana |
Options |
TSelectFolderOptions |
Conjunto de opciones |
StatusText |
String |
Texto para mostrar en la línea de estado |
RootFolder |
TRootFolder |
Carpeta que actuará como raíz |
Path |
String |
Camino actualmente seleccionado |
DisplayName |
String |
Nombre del elemento seleccionado |
Handle |
HWND |
Manejador del cuadro de diálogo |
OKEnabled |
Bolean |
Controla el estado del botón Aceptar |
Icon |
TIcon |
Icono asociado al elemento elegido |
About |
String |
Típico mensaje ‘Acerca de …’ |
Las propiedades Title
, Options
, StatusText
, RootFolder
, Path
y About
estarán disponibles en modo de diseño, accesibles en el Inspector de objetos. El resto, por el contrario, serán propiedades a las que sólo podremos acceder en ejecución. DisplayName
, Handle
e Icon
son propiedades sólo de lectura, mientras que OKEnabled
es sólo de escritura.
Aparte de las propiedades, nuestro componente generará tres eventos: OnInitialized
, OnSelChanged
y OnValidateFailed
. Éstos, como puede observar, se corresponden con los tres mensajes que podíamos recibir en la función de proceso de mensajes. Aportar estos eventos permitirá al usuario del componente realizar operaciones que, de otra forma, no podría efectuar. Cada vez que se cambie la selección en el cuadro de diálogo, por ejemplo, el usuario puede aprovechar el evento OnSelChanged
para saber qué elemento se ha elegido y, en consecuencia, activar o desactivar el botón Aceptar.
Por último, para completar el desarrollo del componente, escribiremos un editor de componentes muy simple, que se limitará a añadir una opción al menú emergente de TSelectFolderDialog
. Mediante dicha opción será posible probar el cuadro de diálogo sin necesidad de escribir código ni ejecutar el programa, viendo de manera inmediata cuál sería el funcionamiento con los valores asignados en ese momento a las propiedades.
Definición previa de tipos
La propiedad Options de nuestro componente será un conjunto de opciones, mediante las cuales el usuario podrá indicar si desea limitar la selección sólo a carpetas del sistema de archivos, mostrar una línea de estado, incluir también archivos, etc. Definiremos, por tanto, una enumeración con constantes que representen todas esas opciones, así como un conjunto que pueda contener dichas opciones.
Mediante la propiedad RootFolder
permitiremos especificar una carpeta raíz, de tal forma que el cuadro de diálogo no muestre todo el espacio de nombres de Windows, sino sólo aquellos elementos que dependan de esa carpeta raíz. Aunque la carpeta raíz podría ser cualquiera, según se vio en el ejemplo previo, lo cierto es que habitualmente se elige una carpeta de sistema, como el escritorio, la carpeta de programas, etc. Por eso RootFolder
no será una cadena, sino un valor de una enumeración que hemos definido como TRootFolder
. En esta enumeración hay una constante por cada carpeta del sistema.
De los tres eventos que ofrecerá este componente, dos de ellos serán de tipo TNotifyEvent
, aportando como único parámetro el conocido Sender
. El evento OnSelChanged
, sin embargo, además de ese parámetro entregará otro, de tipo String
, con el camino correspondiente al elemento elegido. Tras las definiciones de las enumeraciones anteriores, como puede verse en el siguiente listado, definimos un nuevo tipo de evento, al que llamamos TSelChangeEvent
.
Definición de clases
Nuestro componente estará derivado directamente de TComponent
, ya que no cuenta con una parte visual ni precisa ninguna funcionalidad ya existente en la VCL. Como es típico en cualquier componente, dispondremos en la parte privada todos los miembros cuya finalidad sea almacenar valores de propiedades, así como los métodos de acceso a esas propiedades en caso de que existan.
Observe que en la parte protegida existen tres métodos virtuales. Éstos serán llamados desde la función de proceso de mensajes del cuadro de diálogo, siendo su finalidad principal generar los eventos en caso de que el usuario del componente los haya solicitado.
Como miembros públicos tenemos el constructor del componente, el método Execute()
y las propiedades que estarán disponibles sólo en ejecución. La última sección de la clase nos sirve para codificar las propiedades que sí serán accesibles en modo de diseño, así como los eventos. Observe que la mayoría de las propiedades leen y escriben sus valores directamente de los miembros de almacenamiento, en lugar de usar métodos de acceso. Tan sólo existen tres excepciones, correspondientes a la asignación de valores a las propiedades OKEnabled
, StatusText
y Path
. Esto es así porque dichas asignaciones han de traducirse en cambios en el cuadro de diálogo, para lo cual, lógicamente, hay que ejecutar algo de código.
Aparte de la clase en el que se define el componente, en el Listado siguiente puede ver que existen otras dos clases adicionales. La primera de ellas, derivada de TPropertyEditor
, será el editor de propiedades para la propiedad About
. Este editor hará que esa propiedad aparezca en el Inspector de componentes pero sea de sólo lectura, así como que tenga asociada una ventana. La segunda es el editor para el propio componente, que hará aparecer una opción en el menú emergente para poder comprobar su funcionamiento incluso en modo de diseño.
Implementación del componente
En la codificación de los distintos métodos del componente existen algunos procesos obvios: en el constructor se inicializan los miembros con sus valores por defecto, los métodos DoXXX()
comprueban si el usuario ha asignado un método a los eventos, generándolos en caso necesario, etc. Todo este código de implementación se encuentra extensamente comentado, por lo que no tendrá problema alguno en comprenderlo.
Las dos partes más importantes de la implementación son el método Execute()
y la función de proceso de mensajes o callback. Vamos a centrarnos en estos elementos, cuyo código se muestra en el Listado siguiente.
La llamada al método Execute()
se produce una vez que el usuario ha asignado a las propiedades los valores que le interesan, valores que ahora deberemos recuperar de sus miembros de almacenamiento y copiar en la estructura de datos TBrowseInfo
. Para poder asignar al miembro ulFlags
de dicha estructura los valores apropiados, hemos tenido que construir una lista con todas las constantes posibles. Éstas se añaden al mencionado miembro, mediante el operador or, en el interior de un bucle que comprueba las opciones elegidas por el usuario.
Para la propiedad RootFolder hemos tenido que hacer algo parecido. Una matriz contiene las constantes que representan a todos los elementos posibles de esa propiedad. En caso de que el valor seleccionado no sea rfNONE
, usamos la función SHGetSpecialFolderLocation()
para obtener el PIDL de la carpeta y usarla como raíz del cuadro de diálogo.
Fíjese en el valor asignado al miembro lParam
del registro TBrowseInfo
. Guardamos en él un puntero a nosotros mismos, al componente, de tal forma que la función de proceso de mensajes, que no forma parte de la clase, pueda acceder a sus miembros sin problemas.
Establecidos todos los parámetros, invocamos a SHBrowseForFolder()
para hacer aparecer el cuadro de diálogo. Hasta que esa función nos devuelva el control, la función callback recibirá varios mensajes que se traducirán en eventos de TSelectFolderDialog
, en respuesta a los cuales podrán asignarse valores a las propiedades OKEnabled
, Path
o StatusText
.
En caso de que el valor devuelto por SHBrowseForFolder()
no sea nulo, recuperamos el camino correspondiente al elemento elegido. Para terminar, usamos la función CoTaskMemFree()
para liberar el PIDL de la carpeta elegida, así como el de la carpeta que se utilizó como raíz.
La función de proceso de mensajes, por su parte, recuperará el puntero facilitado en el miembro lParam
al llamar a SHBrowseForFolder()
. Disponiendo de ese puntero, traducirá el mensaje en la llamada a uno de los métodos DoXXX()
del componente. En el caso del mensaje BFFM_SELCHANGED
, no obstante, antes se usa la función SHGetFileInfo()
para recuperar el icono correspondiente al elemento elegido, icono que se asigna a la propiedad Icon del componente.
Implementación de los editores
Además del propio componente, en nuestro módulo de código principal deberemos también implementar los métodos correspondientes a los dos editores: el de la propiedad About y el del componente. El código, en ambos casos, es muy simple, como puede verse en el Listado siguiente.
El método GetAttributes()
del editor de la propiedad About devuelve un conjunto de valores, indicando que esta propiedad sólo es de lectura y que, además, contará con un cuadro de diálogo. El método GetValue()
devuelve el valor a mostrar en el Inspector de objetos, mientras que el método Edit()
hará aparecer el mencionado cuadro de diálogo que, en este caso, será una simple ventana con un mensaje.
La implementación del editor del componentes es, si cabe, más simple aún. Usamos los métodos GetVerbCount()
y GetVerb()
para indicar que vamos a añadir una opción al menú emergente y facilitar el texto de dicha opción, respectivamente. Cuando el usuario abra el mencionado menú y ejecute esa opción, el método ExecuteVerb()
se encargará de moldear el componente, cuya referencia se encuentra en Component
, como un TSelectFolderDialog
y llamar a su método Execute()
. Esto causará que realmente se ejecute dicho método, a pesar de que nos encontremos en modo de diseño.
Por último, y un módulo independiente del de implementación, codificaremos la función Register()
, registrando tanto el componente como los editores. Desde dicho módulo, llamado RegistroComponentes.pas, se importarán los módulos que contengan los componentes, por lo que bastará con instalar el módulo de registro para crear el paquete correspondiente e instalar los componentes en Delphi.
Probando el componente TSelectFolderDialog
La forma más simple de comprobar el funcionamiento de este componente, una vez instalado en la Paleta de componentes, consiste en establecer algunas propiedades, desplegar el menú emergente y seleccionar la primera opción disponible, como puede verse en la Figura 3.
Si quiere algo más elaborado, no tiene mas que aprovechar los eventos del componente y escribir algo de código. Con un mínimo de código, se utiliza el componente para elegir una carpeta, un ordenador o una impresora desde el mismo programa. Además, se aprovecha el evento OnSelChanged
para recuperar el icono del elemento elegido, disponiéndolo como icono del formulario.
Con algo más de código, podría efectuar cualquier otra operación que se le ocurra, como examinar los elementos elegidos para activar o desactivar el botón, añadir una línea de estado, etc.