Apunte de Visual C++

 

Por: Demian Panello        demianpanello@yahoo.com.ar

     

Capítulo X

Indice rápido del capítulo 10:

 

DIBUJAR Y REDIBUJAR (la clase CPaintDC, el mensaje WM_PAINT):

 

La clase CPaintDC es un contexto de dispositivo especial, derivada de CDC, que sirve de ayuda para manejar el mensaje WM_PAINT procedente de Windows. El mensaje WM_PAINT se envía a las ventanas cuando dejan de estar cubiertas totalmente o en parte por otra ventana; indica que la aplicación debe redibujar la región descubierta.

En lugar de volver a pintar toda la ventana cada vez que se descubre alguna parte, WM_PAINT nos pasa un rectángulo con las coordenadas del pedacito descubierto. Entonces se puede emplear esta información para volver a dibujar solamente la parte afectada.

 

Para ver esto crearemos un proyecto MFC, basado en diálogos llamado “graf3”.

 

Realice las misma modificaciones al diálogo igual que en el ejemplo del capítulo anterior; esto es cambie el botón Cancelar por Dibujar con ID=IDC_DIBUJAR.

 

El código que vamos a escribir será muy similar al escrito en el ejemplo anterior, pero en principio no lo escribiremos directamente en el evento OnDibujar de botón Dibujar, sino que crearemos una función que luego llamaremos al pulsar el botón y además en el evento OnPaint, para manejar el mensaje WM_PAINT.

 

Para crear la función en ClassView pulse con el botón derecho sobre la clase CGraf3Dlg y seleccione del menú contextual la opción Add Member Function, luego en Function Type escriba void y en Function Declaration escriba Dibujar. Pulse OK y se editará la función, allí escriba:

 

void CGraf3Dlg::Dibujar()

{

            CPaintDC paintDC(this);  (1)       //DC para pintar sobre el diálogo.

            RECT *pRect;            (2)                   //Puntero a la clase RECT para almacenar

                                                                                    //los puntos de la región a redibujar.

            int x,y;

//Se asocia el puntero pRect a la variable m_ps, miembro

            //de CPaintDC, que tiene las coordenadas de la región.

            pRect=&paintDC.m_ps.rcPaint;             (3)  

            //Se recorre para pintar, unicamente la región descubierta.

            for (x=pRect->left; x<pRect->right; x++)

                        for (y=pRect->top; y<pRect->bottom; y++)

                                    paintDC.SetPixel(x,y,RGB(x*y,0,0));

}

 

 

Bien, en primero lugar se declara una variable objeto CPaintDC para pintar sobre el diálogo (1).

En (2) se declara una variable de tipo RECT, (también existe una clase CRect), para almacenar los puntos de la región a redibujar. RECT es una estructura predefinida en VC cuyos campos son:

 

typedef struct tagRECT {   LONG left;   LONG top;   LONG right;   LONG bottom;} RECT;

 

left – indica la coordenada x del punto superior izquierdo.

top – indica la coordenada y del punto superior izquierdo.

right – indica la coordenada x del punto inferior izquierdo.

bottom – indica la coordenada y del punto inferior izquierdo.

 

Lo siguiente a hacer es cargar la estructura pRect con los puntos de la región, (rectángulo), a redibujar; esto se hace en la línea (3) ya que en el mismo objeto CPaintDC creado existe una variable miembro pública llamada m_ps de tipo PAINTSTRUCT que contiene información que le sirve a la aplicación para pintar un área cliente de un diálogo, (ventana), asociado con un objeto CPaintDC. Una estructura PAINTSTRUCT contiene los siguientes campos:

 

typedef struct tagPAINTSTRUCT {   HDC  hdc;   BOOL fErase;   RECT rcPaint;   BOOL fRestore;   BOOL fIncUpdate;   BYTE rgbReserved[16];} PAINTSTRUCT  

 

hdc – identifica el dispositivo de contexto usado para pintar.

fErase – especifica si el fondo de la ventana necesita redibujarse. Si es distinto de 0, es porque la aplicación debería redibujar el fondo.

rcPaint – especifica las esquinas superior izquierda e inferior derecha del rectángulo que necesita ser redibujado. Este es el campo que usamos en la línea (3).

fRestore – campo reservado. Windows lo usa internamente.

fIncUpdate – campo reservado. Windows lo usa internamente.

rgbReserved[16] – bloque de memoria reservado que usa Windows internamente.

 

Lo que resta es recorrer el rectángulo pRect por medio de unos bucles y pintarlo nuevamente con la función SetPixel().

Esto es todo lo que respecta a la función Dibujar, ahora hay que llamarla desde el evento OnDibujar() y desde OnPaint(). Entonces pulse dos veces sobre el botón Dibujar, aparece un aviso indicando que está por crear un manejador de mensajes llamado OnDibujar(), acepte el aviso y escriba:

 

void CGraf3Dlg::OnDibujar()

{

            // TODO: Add your control notification handler code here

            Dibujar();   //Aquí está la llamada a nuestra función.

}

Para escribir la llamada en OnPaint(), pulse dos veces en dicha función en el ClassView.

void CGraf3Dlg::OnPaint()

{

if (IsIconic())

            {

               CPaintDC dc(this); // device context for painting

                //Resumiendo un poco el código

    ....   

               // Draw the icon

              dc.DrawIcon(x, y, m_hIcon);

            }

            else

            {

                        Dibujar();        //Esta es la llamada.

                        CDialog::OnPaint();

            }

}

 

Ejecute la aplicación y obtendrá más o menos la siguiente salida:

 

 

Pruebe superponer ciertas zonas de la ventana con otra, (ejemplo la calculadora), y verá que cuanto más pequeña sea la zona que cubre y descubre más rápido será repintada.

 

 

Resumiendo:

Descargar archivos fuentes del ejemplo: graf3.zip 

 

LÁPICES Y PINCELES (las clases CPen y CBrush):

 

El GDI de Windows proporciona además de cientos de funciones gráficas para dibujar líneas, círculos, elipses etc., herramientas con las cuales dibujarlas y pintarlas. Estas herramientas son los lápices y los pinceles, las clases CPen y CBrush.

¿Cuándo usar lápices y/o pinceles?.

Bueno, si uno quiere, por ejemplo,  dibujar una línea roja de un cierto espesor debe usar un lápiz y especificarle que será de color rojo y que el trazo será de determinado espesor.

Si se quiere pintar el interior de un circulo de color verde, se debe crear un pincel, (Brush), y especificarle tal color.

 

En la práctica, el método operativo consiste en definir una variables CPen para el nuevo lápiz y un puntero CPen para almacenar el lápiz actual, (esto siempre es conveniente hacerlo, y debe ser un puntero la variable puesto que la función que permite guardar el lápiz retorna un puntero). Luego se crea el lápiz, (CreatePen()), especificándole las características deseadas, (estilo, ancho, color); finalmente se guarda el lápiz actual y selecciona el nuevo, (estos dos pasos lo hace la función SelectObject() de un saque).

 

El ejemplo con el que vamos a ver todo esto tendrá como objetivo que el usuario pueda dibujar líneas con sólo pulsar el botón izquierdo en un punto de la ventana y luego en otro para que se realice el trazo entre ellos. Además cada línea que se dibuje tendrá un color aleatorio.

 

 

Se podrán dibujar cuantas líneas uno quiera y si se desea limpiar la ventana, (que desaparezcan todas las líneas) bastará con sólo pulsar el botón derecho.

Para ver el tema de los pinceles haremos que al hacer doble click con el botón izquierdo del mouse se pinte la ventana con un color aleatorio.

 

Cree, entonces, con el AppWizard un proyecto MFC basado en diálogos con el nombre “lineas”.

 

Análisis del programa:

 

En principio necesitamos agregar funciones para gestionar los mensajes WM_LBUTTONDOWN, WM_LBUTTONDBLCLK y WM_RBUTTONDOWN que permitirán dibujar, pintar la ventana y limpiar. Así que agregue estas funciones siguiendo los pasos acostumbrados con el ClassWizard, (CTRL+W y agregar las funciones).

Para poder realizar el trazo entre los puntos indicados por el usuario se necesitará una variable global que almacene el primer punto. Esta variable será de tipo CPoint, la cual tiene dos campos miembros: x e y, donde almacenaremos las coordenadas. También necesitaremos otra variable global para poder detectar si es la primera vez que se pulsa el mouse sobre la ventana y en consecuencia se guarda el punto y no se dibuja la línea.

Los primeros pasos entonces son:

 

-         Pulse CTRL+W para acceder a ClassWizard y agregue las funciones WM_LBUTTONDOWN, WM_LBUTTONDBLCLK  y WM_RBUTTONDOWN a la clase CLineasDlg.

-         Pulse con el botón derecho del mouse sobre la clase CLineasDlg en ClassView y seleccione la opción Add member variable. Cree una llamada ptoAnterior de tipo CPoint y otra llamada Primero de tipo BOOL.

-         Pulse dos veces sobre el constructor de la clase CLineasDlg, (el primer ítem al expandir la rama de la clase). Allí escriba: Primero = TRUE; (valor inicial para la variable Primero).

 

Luego de estos primeros pasos estamos en condición de escribir el código correspondiente al mensaje WM_LBUTTONDOWN, el mismo que se encargará de dibujar la línea.

Expanda la clase CLineasDlg en el ClassView y pulse dos veces sobre la función miembro OnLButtonDown, se editará la función, escriba:

 

void CLineasDlg::OnLButtonDown(UINT nFlags, CPoint point)

{

            // TODO: Add your message handler code here and/or call default

            CClientDC dlgDC(this);            (1)

            CPen* oldLapiz;                      (2)

            CPen Lapiz;                          (3)

            int r, v, a;

           

            //Valores aleatorios para los colores rojo, verde y azul.

            r=rand()%255;                        (4)

            v=rand()%255;                        (5)

            a=rand()%255;                        (6)

            Lapiz.CreatePen (PS_SOLID,2,RGB(r,v,a));            (7)

            oldLapiz = dlgDC.SelectObject (&Lapiz);                        (8)

            if (Primero)            //Si es la primera vez que se pulsa el mouse.

            {

                        ptoAnterior=point;

                        Primero=FALSE;

            }

            else

            {

                        dlgDC.MoveTo (ptoAnterior);            (9)

                        dlgDC.LineTo (point);                         (10)

                        ptoAnterior=point;                                (11)

            }

            dlgDC.SelectObject (oldLapiz);                   (12)

            CDialog::OnLButtonDown(nFlags, point);

}

 

 

Tenemos que dibujar, y la salida será sobre el diálogo, entonces en (1) creamos un contexto de dispositivo para nuestro diálogo. En la línea (2) se define un puntero a la clase CPen para luego guardar el lápiz actual y en (3) una variable CPen para el nuevo.

Como la idea es que cada línea que se dibuje tenga un color diferente en (4), (5) y (6) se toman valores aleatorios para las variables r, (rojo), v, (verde) y a, (azul), que luego serán utilizadas en la función RGB(). La función rand() devuelve un valor aleatorio entre 0 y 1, si tomamos el resto de dividir este número por 255 obtendremos un valor entre 0 y 255. Un tema a tener en cuenta es que, obtendremos valores aleatorios para cada variable durante la ejecución del programa, pero estos mismos valores aleatorios se repetirán en cada ejecución, a menos que de alguna forma le indiquemos a la máquina que “regenere”, (en VB esto se hace con Randomize), en cada ejecución la lista de números aleatorios. Los números aleatorios se obtienen haciendo una cuenta con la hora del sistema, entonces para lograr que en cada ejecución se obtengan nuevos valores aleatorios hay que escribir una sentencia en el mensaje OnInitDialog(), que más adelante se presenta.

Continuando con el análisis del código, en (7) utilizamos la función CreatePen() de la clase CPen, cuyos parámetros son: el estilo, el ancho y el color.

 

BOOL CreatePen( int nPenStyle, int nWidth, COLORREF crColor );

 

nPenStyle:

 

Especifica el estilo del trazo del lápiz, de acuerdo a las siguientes constantes:

 

PS_SOLID:  Trazo sólido.

PS_DASH: Trazo discontinuo en líneas, (el ancho debe ser 1 o menos).

PS_DOT: Trazo discontinuo en puntos, (el ancho debe ser 1 o menos).

PS_DASHDOT: Trazo discontinuo alternando líneas y puntos, (el ancho debe ser 1 o menos).

PS_DASHDOTDOT: Trazo discontinuo alternando líneas con dos puntos, línea+punto+punto+línea+..., (el ancho debe ser 1 o menos).

PS_NULL: Crea un lápiz nulo.

PS_INSIDEFRAME: Crea un lápiz que escribe sólo dentro de los límites de alguna figura dibujada con alguna función GDI, como: Ellipse(), Rectangle(), Chord(), etc.

 

El color del lápiz lo establecemos con la función RGB(), combinando los valores aleatorios para r, v y a anteriormente tomados.

 

La función del contexto de dispositivo, (en nuestro caso el contexto está representado por la variable dlgDC), SelectObject() se encarga, por un lado de seleccionar como activo para el contexto de dispositivo el objeto que se le pase como parámetro, en este caso, el nuevo lápiz y además retorna el lápiz actual, el cual guardamos para luego, con esta misma función volver a activarlo. Esto es lo que hace la línea (8).

 

Se verifica si es la primer pulsación del  botón izquierdo del mouse, ya que de ser así guardamos como punto anterior, (ptoAnterior), el punto donde fue pulsado el mouse en el diálogo, el cual está almacenado en el parámetro point de tipo CPoint del evento OnLButtonDown().

En caso contrario, (no es la primera vez que se pulsa el mouse), es porque por lo menos una vez ya ha sido pulsado, lo que implica que la variable ptoAnterior ya tiene almacenado un valor, (el último punto donde fue pulsado el mouse), entonces en (9) movemos el cursor gráfico a dicho punto anterior y desde allí “tiramos” una línea en (10) hasta el nuevo punto que es la última pulsación y se encuentra almacenada en el parámetro point, (la función LineTo() de la clase CClientDC dibuja una línea desde el punto donde se encuentra el curso grafico, el cual es obviamente invisible, hasta el punto pasado como parámetro), el cual en (11) almacenamos como punto anterior, para la siguiente pulsación.

En (12) seleccionamos nuestro lápiz anterior.

 

Aún nos falta escribir el código que se encarga de limpiar la pantalla al pulsar el botón derecho, el código que se encarga de pintar con diferentes colores el fondo de la ventana al hacer doble click y una pequeña porción de código en OnInitDialog() para mostrar un mensaje informativo antes de empezar y regenerar los números aleatorios.

Veamos el código a escribir en OnInitDialog().

BOOL CLineasDlg::OnInitDialog()

{

            CDialog::OnInitDialog();

            CString Texto;  (1)

            Texto="DBLCLICK cambia el color de fondo\n";  (2)

            Texto=Texto+ "CLICK dibuja una línea\n";  (3)

            Texto= Texto+"CLICK CON EL DERECHO limpia la pantalla"; (4)

 

            // Set the icon for this dialog.  The framework does this automatically

            //  when the application's main window is not a dialog

            SetIcon(m_hIcon, TRUE);                                    // Set big icon

            SetIcon(m_hIcon, FALSE);                      // Set small icon

           

            // TODO: Add extra initialization here

            srand( (unsigned)time(NULL));  (5)

            MessageBox(Texto, "Graficos", MB_ICONINFORMATION);  (6)

            return TRUE;  // return TRUE  unless you set the focus to a control

}

 

En (1) se declara una variable de tipo CString a la que en (2), (3) y (4) se le asigna un texto para mostrar en (6) con la función MessageBox().

En (5) se llama a la función srand() pasándole como parámetro la función time() lo que permite “regenerar” la cuenta para la obtención de números aleatorio.

 

Ahora pulse dos veces en ClassView sobre el método miembro de CLineasDlg,  OnRButtonDown() para editar el evento y allí escribiremos el código que permite limpiar la ventana, o sea borrar todas las líneas que se han dibujado. Escriba:

void CLineasDlg::OnRButtonDown(UINT nFlags, CPoint point)

{

            // TODO: Add your message handler code here and/or call default

            Primero=TRUE;  //Como se borra el dibujo, se vuelve a desde el principio

            RedrawWindow(NULL, NULL, RDW_ERASE|RDW_INVALIDATE);  (1)

            CDialog::OnRButtonDown(nFlags, point);

}

Simplemente se le vuelve a asignar a la variable booleana Primero el valor TRUE, puesto que se va a empezar de nuevo a dibujar y además, (1), se usa la función RedrawWindow() que ya fue explicada en el capítulo anterior aunque aquí sólo se limpia el diálogo cliente y no todo el escritorio.

Todavia falta escribir el código en el evento OnLButtonDblClk(), pero puede probar la aplicación y experimentar dibujando varias líneas.

 

 

 

Pinceles o Brochas (la clase CBrush):

 

Para terminar nuestro pequeño programa de dibujo, tenemos que escribir el código que permita, al hacer doble click, pintar el diálogo de un color aleatorio. ¿Qué se necesita?. Primero que todo crear el objeto CClientDC, luego declarar una variable  CBrush, que será nuestro pincel y crearlo por medio de la función CreateSolidBrush(). Un pincel pinta una figura o una región, entonces, de alguna manera debemos indicarle cual es nuestra región. Esta no es ni más ni menos que el rectángulo que da forma a nuestro diálogo cliente; para obtener sus dimensiones se declara una variable de tipo CRect, (ver página 68),que luego “rellenamos” con la función GetClientRect().

 

Vaya a ClassView y pulse dos veces sobre el procedimiento miembro OnLButtonDlbClk(), allí escriba:

void CLineasDlg::OnLButtonDblClk(UINT nFlags, CPoint point)

{

            // TODO: Add your message handler code here and/or call default

            CClientDC dlgDC(this);

            CBrush Pincel;  (1)

            CRect dlgRect; //Objeto CRect para identificar la ventana

            int r, v, a;

 

            r=rand()%255; //Valores aleatorios para los colores

            v=rand()%255;            (2)

            a=rand()%255;

           

            Pincel.CreateSolidBrush (RGB(r,v,a));            (3)

            GetClientRect(dlgRect);                                   (4)

            dlgDC.FillRect (dlgRect,&Pincel);                   (5)

 

            CDialog::OnLButtonDblClk(nFlags, point);

}

 

 

Bien, además de crear nuestro objeto CClientDC, declaramos una variable de tipo CBrush para el pincel, una de tipo CRect para almacenar las dimensiones de nuestra ventana y algunas enteras para guardar los números aleatorios (2).

En (3) creamos el pincel con la función CreateSolidBrush() miembro de CBrush. El único parámetro que necesita es el color.

Como para pintar se necesita una figura o una región específica, debemos obtener las dimensiones del diálogo y guardar los resultados, (las coordenadas de los dos puntos en diagonal que forman un rectángulo), en la variable CRect. Esto lo realiza la función GetClientRect() cuyo parámetro es una variable de la clase CRect o una estructura RECT donde almacena los datos.

Con la función FillRect() de la clase CClientDC, finalmente pintamos. Los parámetros son: la variable CRect, o sea la región a pintar, (puesto que FillRect() sólo pinta rectángulos) y la dirección de memoria de la variable CBrush, o sea, nuestro pincel.

Listo, pruebe la aplicación y verá que haciendo doble click se pinta el diálogo siempre con un color diferente.

 

 

  Resumiendo:

·        Se puede dibujar una línea por medio de la función LineTo() de la clase CDC, (o CClientDC, esta última es una derivada de la primera). La línea se dibujar partiendo del punto actual donde se encuentre el cursor gráfico hasta el punto, (variable CPoint), pasado como parámetro.

·        Para cambiar la posición del cursor gráfico se utiliza la función MoveTo() cuyo parámetro es el punto a donde debe desplazarse.

·        El color de una línea los especifica el lápiz actual, para utilizar otro se define una variable CPen para el nuevo lápiz y un puntero CPen para guardar temporalmente el lápiz actual. Luego se crea el lápiz con CreatePen(), función de CPen, y se lo selecciona con SelectObject(), función de CDC. SelectObjet() además permite guardar el lápiz anterior.

·        Luego de dibujar, nuevamente con SelectObject() recuperamos el lápiz anterior.

·        Para pintar una figura se utilizan los pinceles o brochas, (clase CBrush), y simplemente hay que declarar una variable CBrush, luego crearla con CreateSolidBrush() y listo. En caso de pintar un rectángulo se utiliza la función FillRect().

Descargar archivos fuentes del ejemplo: lineas.zip

Capítulo siguiente (11).

Capítulo anterior (9).

Página principal.