Torre de Babel

Cómo crear un componente de selección de carpetas con Delphi

by Francisco Charte.

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.

Uso de SHBrowseForFolder()

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.

Miembros del registro TBrowseInfo
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.

procedure TForm1.Button1Click(Sender: TObject);
var
  FBrowseInfo: TBrowseInfo; // Registro para SHBrowseForFolder()
//  SelectFolderDialog1.Execute;
  pidlCarpeta: PItemIDList; // PIDL de la carpeta elegida
  pDisplayName: array[0..MAX_PATH] of Char; // Nombre
  pPath: array[0..MAX_PATH] of Char; // Camino
  IDesktop: IShellFolder; // Interfaz del escritorio
  pidlDesktop: PItemIDList; // PIDL de la carpeta inicial
  dwAtributtes, pchEaten: Cardinal; // Parámetros adicionales
begin
  // Obtenemos la interfaz del escritorio
  SHGetDesktopFolder(IDesktop);
  // Recuperamos el PIDL correspondiente al camino 'C:\'
  IDesktop.ParseDisplayName(0, Nil, 'C:\', pchEaten,
    pidlDesktop, dwAtributtes);
 
  // Inicializamos la estructura estableciendo
  FillChar(FBrowseInfo, sizeof(FBrowseInfo), 0);
  FBrowseInfo.hwndOwner := Handle; // la ventana padre
  // el espacio para recuperar el nombre del objeto
  FBrowseInfo.pszDisplayName := pDisplayName;
  // sólo permitimos seleccionar carpetas
  FBrowseInfo.ulFlags := BIF_RETURNONLYFSDIRS;
  // partiendo desde la unidad C:\
  FBrowseInfo.pidlRoot := pidlDesktop;
 
  // Mostramos el cuadro de diálogo recuperando el PIDL
  pidlCarpeta := SHBrowseForFolder(FBrowseInfo);
  // Si se ha elegido una carpeta
  if pidlCarpeta <> nil then begin
    // recuperamos el camino correspondiente al PIDL
    SHGetPathFromIDList(pidlCarpeta, pPath);
    // y lo mostramos
    ShowMessage(pPath);
    CoTaskMemFree(pidlCarpeta); // liberamos el PIDL
  end;
 
  // Liberamos el PIDL correspondiente a C:\
  CoTaskMemFree(pidlDesktop);
end;

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.

Lista de propiedades del componente TSelectFolderDialog
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.

type
  // Enumeración de las opciones aplicables al cuadro de diálogo
  TSelectFolderOption = (foBrowseForComputer, foBrowseForPrinter,
    foBrowseIncludeFiles, foDontGoBelowDomain, foEditBox,
    foReturnFSAncestors, foReturnOnlyFSDirs, foStatusText,
    foValidate);
 
  // Conjunto para la propiedad Options del componente
  TSelectFolderOptions = Set Of TSelectFolderOption;
 
  // Enumeración de las carpetas que pueden actuar como raíz
  TRootFolder = (
    rfNONE, rfDESKTOP, rfINTERNET, rfPROGRAMS, rfCONTROLS,
    rfPRINTERS, rfPERSONAL, rfFAVORITES, rfSTARTUP, rfRECENT,
    rfSENDTO, rfBITBUCKET, rfSTARTMENU, rfDESKTOPDIRECTORY,
    rfDRIVES, rfNETWORK, rfNETHOOD, rfFONTS, rfTEMPLATES,
    rfCOMMON_STARTMENU, rfCOMMON_PROGRAMS, rfCOMMON_STARTUP,
    rfCOMMON_DESKTOPDIRECTORY, rfAPPDATA, rfPRINTHOOD, rfALTSTARTUP,
    rfCOMMON_ALTSTARTUP, rfCOMMON_FAVORITES, rfINTERNET_CACHE,
    rfCOOKIES, rfHISTORY);
 
  // Tipo para el evento de cambio de selección
  TSelChangeEvent = procedure(Sender: TObject; Path: String) of object;

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.

// Clase de definición del componente
  TSelectFolderDialog = class(TComponent)
  private
    // Miembros de almacenamiento de propiedades
    FDisplayName: String; // Nombre del elemento seleccionado
    FPath: String; // Camino del elemento seleccionado
    FTitle: String; // Título en el cuadro de diálogo
    FOptions: TSelectFolderOptions; // Opciones
    FHandle: HWND; // Identificador del cuadro de diálogo
    FStatusText: String; // Texto de la línea de estado
    FRootFolder: TRootFolder; // Carpeta raíz
    FIcon: TIcon; // Icono que representa al elemento seleccionado
    FAbout: String; // Para la propiedad About
 
    // Miembros para los eventos
    FOnValidateFailed: TNotifyEvent;
    FOnSelChanged: TSelChangeEvent;
    FOnInitialized: TNotifyEvent;
 
    // Métodos de acceso a las propiedades
    procedure SetOKEnabled(const Value: Boolean);
    procedure SetStatusText(const Value: String);
    procedure SetPath(const Value: String);
  protected
    // Métodos para procesar los mensajes del cuadro de diálogo
    procedure DoInitialized; virtual;
    procedure DoSelChanged(Path: String); virtual;
    procedure DoValidateFailed; virtual;
  public
    // Redefinimos el constructor
    constructor Create(AOwner: TComponent); override;
    // Método que mostrará el cuadro de diálogo
    function Execute: Boolean;
 
    // Propiedades accesibles sólo en ejecución
    property DisplayName: String read FDisplayName;
    property Handle: HWND read FHandle;
    property OKEnabled: Boolean write SetOKEnabled;
    property Icon: TIcon read FIcon;
  published
    // Propiedades accesibles en modo de diseño
    property Title: String read FTitle write FTitle; // Título ventana
    // Opciones modificadoras
    property Options: TSelectFolderOptions read FOptions write FOptions
      default [foReturnOnlyFSDirs, foStatusText];
    // Texto en la línea de estado
    property StatusText: String read FStatusText write SetStatusText;
    // Carpeta raíz
    property RootFolder: TRootFolder read FRootFolder write FRootFolder
      default rfNONE;
    property Path: String read FPath write SetPath; // Camino actual
    property About: String read FAbout write FAbout; // About
 
    // Eventos del componente
    property OnInitialized: TNotifyEvent
     read FOnInitialized write FOnInitialized;
    property OnSelChanged: TSelChangeEvent
      read FOnSelChanged write FOnSelChanged;
    property OnValidateFailed: TNotifyEvent
      read FOnValidateFailed write FOnValidateFailed;
  end;
 
  // Clase que actuará como editor de la propiedad About
  TAboutPropertyEditor = class(TPropertyEditor)
  public // Estableceremos los atributos necesarios
    function GetAttributes: TpropertyAttributes; override;
    function GetValue: String; override; // para devolver una cadena
    procedure Edit; override; // y mostrar una ventana asociada
  end;
 
  // Clase que actuará como editor del componente
  TSelectFolderDialogEditor = class(TDefaultEditor)
  public // añadiremos una opción al menú emergente para
    procedure ExecuteVerb(Index: Integer); override; // probar el cuadro
    function GetVerb(Index: Integer): string; override;
    function GetVerbCount: Integer; override;
  end;
  

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.

// Al llamar al método Execute() del componente
function TSelectFolderDialog.Execute: Boolean;
const
  // Valores que representan a las opciones de
  // la propiedad Options
  foValores: array[TSelectFolderOption] of UINT =
      (BIF_BROWSEFORCOMPUTER, BIF_BROWSEFORPRINTER,
       BIF_BROWSEINCLUDEFILES, BIF_DONTGOBELOWDOMAIN,
       BIF_EDITBOX, BIF_RETURNFSANCESTORS,
       BIF_RETURNONLYFSDIRS, BIF_STATUSTEXT, BIF_VALIDATE);
 
  // Valores que representan a las opciones de la
  // propiedad RootFolder
  rfValores: array[TRootFolder] of Integer = (
    -1, CSIDL_DESKTOP, CSIDL_INTERNET, CSIDL_PROGRAMS, CSIDL_CONTROLS,
    CSIDL_PRINTERS, CSIDL_PERSONAL, CSIDL_FAVORITES, CSIDL_STARTUP,
    CSIDL_RECENT, CSIDL_SENDTO, CSIDL_BITBUCKET, CSIDL_STARTMENU,
    CSIDL_DESKTOPDIRECTORY, CSIDL_DRIVES, CSIDL_NETWORK, CSIDL_NETHOOD,
    CSIDL_FONTS, CSIDL_TEMPLATES, CSIDL_COMMON_STARTMENU,
    CSIDL_COMMON_PROGRAMS, CSIDL_COMMON_STARTUP,
    CSIDL_COMMON_DESKTOPDIRECTORY, CSIDL_APPDATA, CSIDL_PRINTHOOD,
    CSIDL_ALTSTARTUP,CSIDL_COMMON_ALTSTARTUP,CSIDL_COMMON_FAVORITES,
    CSIDL_INTERNET_CACHE, CSIDL_COOKIES, CSIDL_HISTORY);
 
var
  pidlRaiz: PItemIDList; // PIDL de la carpeta raíz
  pidlCarpeta: PItemIDList; // PIDL de la carpeta elegida
  pDisplayName: array[0..MAX_PATH] of Char; // Nombre
  pPath: array[0..MAX_PATH] of Char; // Camino
  foOpcion: TSelectFolderOption;
  FBrowseInfo: TBrowseInfo; // Estructura para SHBrowseForFolder()
begin
  // Inicializamos la estructura TBrowseInfo
  FillChar(FBrowseInfo, sizeof(FBrowseInfo), 0);
  // estableciendo la ventana padre
  FBrowseInfo.hwndOwner := Application.Handle;
  // el espacio para recuperar el nombre del objeto
  FBrowseInfo.pszDisplayName := pDisplayName;
  // Establecemos la función de retorno
  FBrowseInfo.lpfn := DialogCallback;
  // Puntero a nosotros mismos como parámetro adicional
  FBrowseInfo.lParam := Integer(Self);
  // Título a mostrar en la ventana
  FBrowseInfo.lpszTitle := PChar(FTitle);
 
  FBrowseInfo.ulFlags := 0; // Indicadores de funcionamiento
  // Recorremos el conjunto para ver qué opciones se han activado
  for foOpcion := foBrowseForComputer to foValidate do
      if foOpcion in FOptions then // por cada una de ellas
          FBrowseInfo.ulFlags := FBrowseInfo.ulFlags or
              foValores[foOpcion]; // añadimos el valor apropiado
 
  // Si se ha elegido una carpeta raíz
  if FRootFolder <> rfNONE then begin
    // obtenemos el PIDL correspondiente
    SHGetSpecialFolderLocation(0, rfValores[FRootFolder], pidlRaiz);
    FBrowseInfo.pidlRoot := pidlRaiz;   // y lo indicamos
  end;
 
  // Mostramos el cuadro de diálogo recuperando el PIDL
  pidlCarpeta := SHBrowseForFolder(FBrowseInfo);
  // Si el PIDL no es nulo es que se ha elegido una carpeta
  Result := pidlCarpeta <> nil;
  // Si se ha elegido una carpeta
  if Result then begin
    SHGetPathFromIDList(pidlCarpeta, pPath); // obtenemos el camino
    FPath := pPath; // y lo asignamos a la propiedad Path
  end;
 
  // Recuperamos el nombre del objeto elegido
  FDisplayName := pDisplayName;
 
  // Liberamos la memoria asignada para el PIDL de la carpeta elegida
  if pidlCarpeta <> Nil Then
    CoTaskMemFree(pidlCarpeta);
  // y de la carpeta raíz si existe
  if pidlRaiz <> Nil then
    CoTaskMemFree(pidlRaiz);
 
  FHandle := 0; // Invalidamos la propiedad Handle
end;
 
// Función que se encargará de procesar los mensajes
// generados por el cuadro de diálogo
function DialogCallback;
var
  pComp: TSelectFolderDialog;
  pPath: array[0..MAX_PATH] of Char;
  shInfo: TShFileInfo;
begin
  Result := 0; // El valor de retorno siempre será cero
 
  // Convertimos el parámetro de usuario en un puntero al objeto
  pComp := TSelectFolderDialog(lpData);
 
  pComp.FHandle := Wnd; // Guardamos el identificador de la ventana
 
  case uMsg of // Dependiendo del mensaje recibido
    BFFM_INITIALIZED: pComp.DoInitialized; // efectuar inicialización
    BFFM_SELCHANGED: begin // cambio de elemento seleccionado
      // Recuperamos el icono asociado al elemento
      SHGetFileInfo(PChar(lParam), 0, shInfo, SizeOf(shInfo),
        SHGFI_ICON or SHGFI_LARGEICON or SHGFI_PIDL);
      pComp.FIcon.ReleaseHandle; // liberamos el icono anterior
      pComp.FIcon.Handle := CopyIcon(shInfo.hIcon); // obtenemos el nuevo
      DestroyIcon(shInfo.hIcon); // y destruímos la copia de SHGetFileInfo
      // Recuperamos el camino correspondiente al elemento
      SHGetPathFromIDList(PItemIDList(lParam), pPath);
      pComp.DoSelChanged(pPath); // y procesamos
    end;
    BFFM_VALIDATEFAILED: pComp.DoValidateFailed;
  end;
end;

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.

{ TAboutPropertyEditor }
 
function TAboutPropertyEditor.GetAttributes;
begin // La propiedad tendrá una ventana asociada y será
    Result := [paDialog, paReadOnly]; // sólo de lectura
end;
 
function TAboutPropertyEditor.GetValue;
begin // Devolvemos el texto que aparecerá en el Inspector de objetos
    Result := '(c) Componente Delphi - 1999';
end;
 
procedure TAboutPropertyEditor.Edit;
begin // mostramos la ventana 'About'
    Application.MessageBox('TSelectFolderDialog' + #13 +
        '(c) Componente Delphi - 1999', 'About ...',
        MB_OK Or MB_ICONINFORMATION);
End;
 
 
{ TSelectFolderDialogEditor }
 
function TSelectFolderDialogEditor.GetVerbCount;
begin
       Result := 1; // Vamos a añadir una sola opción al menú emergente
end;
 
function TSelectFolderDialogEditor.GetVerb;
begin
  Result:='Probar cuadro de diálogo'; // Cono este texto
end;
 
procedure TSelectFolderDialogEditor.ExecuteVerb;
begin // al elegir esa opción
  (Component As TSelectFolderDialog).Execute; // mostramos el cuadro
end;

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.