Hojas de propiedades modales.
Por: Demian Panello.
Hojas de Propiedades.
MFC provee una clase llamada CPropertySheet que permite visualizar hojas de propiedades, como por ejemplo la que se ve cuando seleccionamos Properties en Visual C++, o en el menú Tools -> Internet Options, (si tiene el explorador de internet en inglés), etc.

Es una ventana con una serie de solapas donde supuestamente se especifican características generales y particulares del programa.
Property Sheets y Property Pages.
Cada "solapa" de una hoja de propiedades, (tome como ejemplo, si quiere, la imagen de arriba), es un diálogo, (entonces según la imagen allí hay 5 diálogos), donde individualmente se diseña cada solapa. Pero estos diálogos tienen que tener las siguientes características, (propiedades):
El Caption establecido será el texto de la solapa, (en la imagen de ejemplo tenemos los diálogos: General, Security, Content, Connections, Programs y Advanced). Los títulos de los diálogo serán los textos de las solapas.
Style debe ser Child.
Border debe ser Thin.
Titlebar debe estar seleccionado.
Disabled en More Styles debe estar seleccionado.
Si usa Visual C++ 6.0 puede agregar un recurso de diálogo ya predefinido para ser usado en Property Sheets o bien agregar un diálogo común y especificar todas estas propiedades.
Cada diálogo debe ser derivado de una clase CPropertyPage, (en el ejemplo tendríamos 5 clases derivadas de CPropertyPage), y luego en el evento que "dispare" la acción de mostrar la hoja de propiedades se agregan a un objeto CPropertySheet cada CPropertyPage con la función AddPage().
Por defecto CPropertyPage nos coloca los botones Aceptar, Cancelar, Aplicar y Ayuda y con DoModal() mostramos la hoja de propiedades de forma modal, (hasta que no la cerremos no podremos acceder al resto de la aplicación).
Lo realmente importante es que los cambios realizados en cada página de propiedades, (CPropertyPage), debe ser controlado en las funciones DoDataExchange de cada página y el trabajo acostumbrado es pasar los contenidos de ciertas variables miembro del diálogo principal, al momento de llamar a la hoja de propiedades, a variables miembros de las páginas de propiedades y después de la llamada pasar, ahora de forma inversa, los valores actualizados de las variables miembros de cada página de propiedades a las variables miembros del diálogo principal.
Aplicación de ejemplo:
Vamos a hacer una aplicación basada en diálogos con un botón que llamará a una hoja de propiedades en la cual podremos especificar el color de fondo del diálogo y el título.
Una vez creada la aplicación vaya a Resource View e inserte dos diálogos IDD_PROPPAGE_SMALL, (si no tiene esta opción, puede agregar dos diálogos comunes y especificarles las propiedades que mencioné arriba). Cuando haya agregado los diálogos y teniendo a la vista el primero de ellos, pulse CTRL+W para activar el Class Wizard, verá que de inmediato se le alerta que acaba de agregar un recurso de diálogo y no hay una clase para el mismo, seleccione "Create a new class". Como nombre de la clase escriba CPage1 y seleccione como clase base CPropertyPage y verifique que la clase se guarde en el archivo CPropertyPage.h. Para el otro diálogo haga lo mismo pero con el nombre de clase CPage2 y el archivo de cabecera también CPropertyPage.h, así no se generan dos archivos de cabecera por cada diálogo, (si no respetó este último paso, usar el mismo archivo .h, no importa).
Ahora diseñaremos cada diálogo. El primero debe tener como Caption = "Color del diálogo" y los siguientes controles:

Marque la propiedad Group del primer Option Button, colóquele el ID = IDC_COLOR y asígnele una variable de tipo int llamada m_iColor.
El siguiente diálogo tendrá el Caption = "Título del diálogo", una variable miembro CString llamada m_sTitDialogo y la siguiente disposición de controles:

| Control | ID | Propiedades | Variable |
| Option Button | IDC_TITULO | Group y Caption = "Ejemplo1 de Property Sheet" | int m_iTitulo |
| Option Button | por defecto | Caption = "Las ruinas circulares" | ninguna |
| Option Button | por defecto | Caption = "La invensión de Morel" | ninguna |
| Check Button | IDC_PERSONALIZADO | Group, Caption = "Título personalizado" | BOOL m_bPersonalizado |
| Edit | IDC_TITEDIT | Disabled | CString m_Titulo |
Agregue al diálogo principal una variable miembro de tipo int llamada iBrush y un puntero a CBrush llamado pNewBrush. En el constructor del diálogo inicialice la variable entera con 0 y reserve memoria para el objeto CBrush como sigue:
pNewBrush = new CBrush
La variable entera indica qué color tendrá el diálogo, por defecto 0 es el color estándar y de acuerdo a lo que luego se seleccione de la hoja de propiedades con lo option buttons, esta variable adquirirá un valor entre 0 y 3, (default, rojo, verde y azul). Y el objeto CBrush que creamos servirá para pintar el diálogo con el color seleccionado.
Agregue el mensaje WM_CTLCOLOR al diálogo principal, ya que éste es el que permite utilizar un objeto CBrush para pintarlo. En él escriba:
| //Mensaje para el cambio de color de un control. HBRUSH CEjemplo1_PSDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { //Pincel por defecto. HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor); //Si lo que se pretende es cambiar el color del diálogo. if (nCtlColor==CTLCOLOR_DLG) //Retorno el nuevo pincel, (ver función CambiaColor()). return (HBRUSH) (pNewBrush->GetSafeHandle()); else //Sino el pincel por defecto. return hbr; } |
Este mensaje sirve para cambiar el color de diversos controles, (pero no los botones), en caso que la variable nCtlColor es CTLCOLOR_DLG, es porque se desea cambiar el color de un diálogo entonces retorno el objeto CBrush pNewBrush sino retorno el pincel por defecto.
Agregue una función de tipo void llamda CambiaColor() y allí escriba:
//Esta función crea el pincel adecuado para el color del diálogo. |
Coloque además un botón con Caption=Propiedades e ID: IDC_PROPIEDADES y en OnPropiedades escriba:
| void
CEjemplo1_PSDlg::OnPropiedades() { CPropertySheet ps; //Creo un objeto CPropertySheet //Creo dos páginas CPage1 y CPage2, (que fueron derivadas de CPropertyPage): CPage1 Page1; CPage2 Page2; //A la variable de los option buttons le asigno el valor actual del nro. de pincel //0 - default, 1 - rojo, 2 - verde, 3 - azul. Page1.m_iColor = iBrush; //Le indico a una variable CString de la página 2 cual es el actual título del diálogo. GetWindowText(Page2.m_sTitDialogo); //Le especifico un título a la PropertySheet. ps.SetTitle ("Propiedades del diálogo"); //Le especifico a la PropertySheet cuales son las páginas que tendrá. ps.AddPage (&Page1); ps.AddPage (&Page2); //Llamo a la PropertySheet y si regresó por haber pulsado Aceptar, (IDOK), entonces... if (ps.DoModal()==IDOK) { //Tomo los nuevos datos de ambas páginas, Page1 y Page2. //Datos de Page1 iBrush=Page1.m_iColor; //Datos de Page2. SetWindowText(Page2.m_sTitDialogo); //Llamo a la función encargada de crear el pincel adecuado. CambiaColor(); } } |
En este procedimiento se crea el objeto CPropertySheet y se le agrega los objetos CPage1 y CPage2, (que recuerde, habían sido derivados de CPropertyPage). Se inicializan las variables miembros de cada página de propiedades y ocurre la llamada modal a la hoja de propiedades.
Cuando se regrese de la hoja de propiedades por pulsar "Aceptar", (DoModal() devuelve IDOK), entonces hay que actualizar el diálogo, por eso se "vuelca" el contenido de la variable miembro m_iColor a iBrush, se especifica como título lo que está en m_sTitDialogo y se llama a CambiaColor(). Fíjese que en CambiaColor() se evalúa el contenido de iBrush para así crear el pincel correcto.
Código en las páginas de propiedades:
En el diálogo que representa la página de propiedad para establecer un nuevo color, no hay que escribir ni una línea de código, ya que no hay nada que evaluar y los botones "Aceptar" y "Cancelar" hacen el trabajo de actualizar la variable m_iColor, ya que hace una llamada a UpdateData(TRUE). En cambio en el diálogo para establecer el título se requiere algo de código ya que tenemos un Check Box que si tildamos debería inhabilitar los botones de opción y habilitar el cuadro de edición, si le quitamos la marca debería habilitar los botones de opción y deshabilitar el cuadro de edición.
Además cuando se inicie el diálogo, (cada vez que se selecciona la solapa), debería estar marcado el botón de opción que corresponda al título actual o bien, el título personalizado en el cuadro de edición con el check box marcado.
Necesitaremos código en OnInitDialog() de este dialogo:
| BOOL CPage2::OnInitDialog() { CPropertyPage::OnInitDialog(); //Inicializaciones. //Tomo el handle del edit para desactivarlo. CEdit* pEdit = (CEdit*) GetDlgItem(IDC_TITEDIT); pEdit->EnableWindow(FALSE); m_bPersonalizado=FALSE; //Veo cual es el título actual del diálogo para así presentar ya seleccionado //el option button correspondiente. if (m_sTitDialogo=="Ejemplo1 de Property Sheet") m_iTitulo=0; else if (m_sTitDialogo=="Las ruinas circulares") m_iTitulo=1; else if (m_sTitDialogo=="La invención de Morel") m_iTitulo=2; else //Si el título es otro, entonces es porque está personalizado. { //Tomo los handles de los option button para deshabilitarlos todos, (ver más abajo). CButton* pOpButton1 = (CButton*) GetDlgItem(IDC_TITULO); CButton* pOpButton2= (CButton*) GetDlgItem(IDC_RADIO3); CButton* pOpButton3= (CButton*) GetDlgItem(IDC_RADIO4); //El check button tiene tilde. m_bPersonalizado=TRUE; //El edit está habilitado. pEdit->EnableWindow(TRUE); //Y el contenido es el título actual del diálogo. m_Titulo=m_sTitDialogo; //A pesar de que están deshabilitados lo option buttons, igual marco el primero, //alguno siempre debe estar marcado. m_iTitulo=0; pOpButton1->EnableWindow(FALSE); pOpButton2->EnableWindow(FALSE); pOpButton3->EnableWindow(FALSE); } //Actualizo los controles. UpdateData(FALSE); return TRUE; } |
Dijimos que al pulsar el check box debíamos verificar si lo marcamos o no para así habilitar y/o deshabilitar otro controles, entonces pulse dos veces con el mouse sobre el check box para generar la función OnPersonalizado(), allí escriba:
| //Al pulsar el check button debo deshabilitar los options y habilitar el
edit. void CPage2::OnPersonalizado() { //Handles del edit y los options buttons. CEdit* pEdit = (CEdit*) GetDlgItem(IDC_TITEDIT); CButton* pOpButton1 = (CButton*) GetDlgItem(IDC_TITULO); CButton* pOpButton2= (CButton*) GetDlgItem(IDC_RADIO3); CButton* pOpButton3= (CButton*) GetDlgItem(IDC_RADIO4); CButton* pChkPer = (CButton*) GetDlgItem(IDC_PERSONALIZADO); //Me fijo el estado del check, (0 sin tilde, 1 con tilde). int chkp=pChkPer->GetCheck (); //Habilito o deshabilito el edit, (si chkp es 0 deshabilito pues no hay tilde en el check. pEdit->EnableWindow (chkp); //Habilito o deshabilito los option negando el estado del check. //Si el check tiene tilde, (chkp=1), los options se deshabilitan y //si el check no tiene tilde, (chkp=0), los options se habilitan. pOpButton1->EnableWindow(!chkp); pOpButton2->EnableWindow(!chkp); pOpButton3->EnableWindow(!chkp); } |
Yo quiero que al presentarse esta página, (CPage2), se muestre en los controles los datos actuales del diálogo principal, por ejemplo: si el diálogo tiene el título "Las ruinas circulares" debería estar marcado el botón de opción correspondiente a ese título. Pero que pasa, cada vez que se selecciona una solapa, (la correspondiente a CPage1 y CPage2), ocurre un llamado a la función DoDataExchange() del diálogo al cual se está "entrando" ya que es la función responsable de actualizar la información, pero siempre y cuando haya sido llamada por haber usado UpdateData(TRUE) que precisamente indica la transferencia de datos desde los controles hacia las variables. Por lo tanto vamos a escribir en DoDataExchange() código que permita discernir si se están actualizando variables o no.
DoDataExchange() recibe un puntero a CDataExchange y por ese puntero podremos acceder a un dato miembro de esta clase para averiguar si ha ocurrido una llamada a UpdateData(TRUE). Escriba entonces en DoDataExchange:
void CPage2::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CPage2)
DDX_Radio(pDX, IDC_TITULO, m_iTitulo);
DDX_Check(pDX, IDC_PERSONALIZADO, m_bPersonalizado);
DDX_Text(pDX, IDC_TITEDIT, m_Titulo);
DDV_MaxChars(pDX, m_Titulo, 30);
//}}AFX_DATA_MAP
//Si ocurrió una llamada a UpdateData(TRUE) entonces llamo a Actualiza().
if (pDX->m_bSaveAndValidate)
Actualiza();
}
Marcada en amarillo las líneas de código agregadas a esta función. El campo m_bSaveAndValidate es TRUE si ha ocurrido la llamada a UpdateData(TRUE) entonces llamamos a una función propia Actualiza().
Agregue la función Actualiza() al diálogo, es de tipo void y en ella escriba:
| //Esta función actualiza las variables miembro de esta página //que serán leídas, luego, en el diálogo principal. void CPage2::Actualiza() { //Si el check no tiene tilde, entonces eligió algún título de los options. if (!m_bPersonalizado) { //Tomo el handle del option button que está seleccionado. CButton* pButton = (CButton*) GetDlgItem(IDC_TITULO + m_iTitulo); //Tomo el texto del option button, ya que ese será el título del diálogo. pButton->GetWindowText(m_sTitDialogo); } else //el check tiene tilde, entonces el título es lo que contiene el edit. m_sTitDialogo=m_Titulo; } |
Bueno, eso es todo. Tuvimos más trabajo en esta página porque no sólo había botones de opción para marcar, (como en la primera), sino que además teníamos un check box e información para leer y/o escribir en un edit.
En otro artículo veremos como crear hojas de propiedades no modales y quizás en otro más, cómo "transformar" una hoja de propiedades en un "wizard". Un wizard son esas series de ventanas que permiten ir definiendo los pasos de por ejemplo una instalación, o generar cosas de forma automática, (sí, como el Aplication Wizard cuando uno crea la estructura del programa, exactamente a esos wizards me refiero), y tienen los botones "Anterior" y "Siguiente", (y al terminar la serie de ventanas, la última tiene el botón "Finalizar" o "Terminar").
Descargar fuente de los ejemplos: Ejemplo1_PS.zip (35 Kb).