45 La impresora

Cuando, en el capítulo 16, hablamos del GDI y de los Contextos de Dispositivo (DCs), comentamos que nuestras aplicaciones Windows no acceden directamente a los dispositivos de salida, sino que lo hacen a través de un interfaz, el DC, de modo que todo lo comentado entre los capítulos 16 y 29 es indiferente de si la salida es una pantalla o una impresora.

Pero esto es en lo que respecta al modo de enviar datos a un dispositivo. Cada impresora, como cada tarjeta gráfica, tiene sus particularidades: resolución, colores, orientación de papel, etc. Además, existen otras funcionalidades que podemos querer añadir a nuestras aplicaciones, como las vistas previas o la selección de páginas a imprimir o el número de copias.

Proceso de impresión

Desde el punto de vista de la aplicación, no existe diferencia entre enviar datos a un DC de pantalla o de impresora. Sin embargo, para el sistema si existen diferencias, ya que la impresora no muestra los gráficos del mismo modo que una pantalla: es más lenta, no permite borrar, no permite superponer unas salidas gráficas a otras, o usan diferentes protocolos (por ejemplo, PostScript).

Por todo ello, normalmente no se envían los datos a la impresora hasta que la aplicación da por finalizada la creación de una página o incluso de un documento completo. Esto hace que deban existir varios procesos intermedios:

El spooler de impresión (print spooler)

Un spooler no es otra cosa que un programa que gestiona un almacén temporal (un buffer) que puede estar almacenado en memoria o en disco duro, donde se almacena la información generada por un proceso o dispositivo y de donde se extrae por otro proceso o dispositivo, generalmente más lento.

En el caso del spooler de impresión, se almacenan los datos necesarios para generar el documento que se quiere imprimir a la espera de que la impresora, mucho más lenta, los procese y obtenga una copia impresa.

El spooler permite también crear colas de impresión. De hecho, esas colas son una necesidad, puesto que el ordenador puede generar documentos a una velocidad mucho mayor de la que la impresora puede imprimirlos, es imprescindible crear un proceso que impida que se mezclen páginas de unos documentos con los de otros, o incluso datos de distintas páginas en la misma.

También hace posible que la aplicación no deje de funcionar porque la impresora no esté conectada o esté temporalmente indisponible, por falta de papel, de tinta o de tonner, por ejemplo. Los documentos se almacenan en la cola y se procesarán cuando la impresora vuelva a estar preparada.

Del mismo modo, permite seleccionar la impresora a la que se envía cada documento. Actualmente, en nuestros ordenadores puede haber conectadas varias impresoras: impresoras locales conectadas a distintos puertos, impresoras de red, impresoras remotas, impresoras virtuales para generar ficheros PDF o ficheros que se pueden enviar por correo o copiar e imprimir en otros lugares, faxes, etc.

El procesador de impresión (print processor)

El procesador de impresión de Windows es una DLL que convierte los registros de un documento almacenado por el spooler a llamadas DDI (interfaz de controlador de dispositivo).

La máquina de gráficos (graphics engine)

Es otra DLL que convierte la salida del procesador a llamadas a funciones de controlador de dispositivo. Este, a su vez, procesa esas llamadas y las convierte en comandos que la impresora puede manejar.

El monitor

Una vez procesador todo el documento, el fichero de comandos de impresora se devuelve al spooler. El spooler es el encargado de enviar esos comandos a un monitor, otra DLL, que envía esos comandos a través del canal adecuado: red, puerto paralelo, serie... al dispositivo.

Todo este proceso es interno, y transparente para nosotros. Nuestra tarea se limita a seleccionar una impresora (o usar la impresora por defecto), y enviarle los documentos que queremos imprimir. El resto es tarea del sistema operativo.

Obtener una lista de impresoras

Aunque en la práctica generalmente usaremos un cuadro de diálogo común (definido por el sistema) para seleccionar tanto la impresora como muchos otros parámetros de impresión: número de copias, rango de páginas, formato de página, etc.; a veces puede ser útil obtener una lista de las impresoras disponibles e infomación sobre cada una de ellas.

Para hacer esto disponemos de una función del API, EnumPrinters y de varias estructuras, dependiendo del tipo de información que precisemos sobre cada impresora.

Como en otras funciones de enumeración, la información de retorno se devuelve mediante un array, apuntado por uno de los parámetros. Pero no hay manera de saber el tamaño de ese array hasta después de retornar, es decir, no tenemos a priori los parámetros necesarios para hacer la llamada.

Para solucionar esta situación EnumPrinters permite una llamada sin usar ese parámetro, y en otro de ellos retorna el tamaño necesario para el array. De este modo, mediante dos llamadas a la función podemos recuperar la información requerida:

    DWORD tamano, numero;
    PRINTER_INFO_4 *info;
...
    EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
        NULL,
        4,
        NULL,
        0,
        &tamano,
        &numero);
    info = (PRINTER_INFO_4*)malloc(tamano);
    EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
        NULL,
        4,
        (BYTE*)info,
        tamano,
        &tamano,
        &numero);
...
    free(info);

Ejemplo 77

Para poder compilar este ejemplo hay que incluir entre las librerías "winspool.lib".

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 77 win077.zip 2010-09-22 2512 bytes 728

Contexto de dispositivo

Ya hemos comentado que para enviar una salida a una impresora necesitamos un DC, pero los DC de impresora no son exactamente iguales que los de ventana que hemos usado hasta ahora. Para empezar, no podemos usar la función GetDC, ya que los DC de impresora no están asociados a una ventana.

Para obtener un DC de impresora podemos usar dos funciones: CreateDC o PrintDlg.

No hay una forma mejor que la otra, se trata de dos opciones que tenemos a nuestra disposición. Tal vez parezca más complicado usar PrintDlg, pero eso sólo es porque ofrece muchas más opciones.

Típicamente, usaremos la primera forma para imprimir una copia completa de un documento. Es la respuesta a la opción "Imprimir" de los menús de las aplicaciones. La segunda opción la usaremos cuando queramos dejar al usuario seleccionar ciertas opciones: páginas a imprimir, número de copias, intercalado, impresora, etc.

Usando CreateDC

Bien, la forma artesanal consiste en conseguir una lista de impresoras para que el usuario elija una, o usar la impresora por defecto. Crear un DC para la impresora, imprimir el documento, y destruir el DC:

    char impresora[256];
    int i;
    DOCINFO di;
    HBITMAP hBitmap;
    HDC memDC;
    BITMAP bm;
...
    hBitmap = (HBITMAP)LoadImage(NULL, "meninas24.bmp", IMAGE_BITMAP,
		0, 0, LR_LOADFROMFILE);
    GetObject(hBitmap, sizeof(BITMAP), (LPSTR)&bm);
    i = SendDlgItemMessage(hwnd, ID_LISTA, LB_GETCURSEL, 0, 0);
    SendDlgItemMessage(hwnd, ID_LISTA, LB_GETTEXT, (WPARAM)i, (LPARAM)impresora);
    hPrinterDC = CreateDC("WINSPOOL", impresora, NULL, NULL);
    di.cbSize = sizeof(DOCINFO);
    di.lpszDocName = "Prueba";
    di.lpszOutput = NULL;
    di.lpszDatatype = NULL;
    di.fwType = 0;
    StartDoc(hPrinterDC, &di);
    StartPage(hPrinterDC);
    TextOut(hPrinterDC, 10, 10, "Hola, mundo!", 12);
    Ellipse(hPrinterDC, 150, 180, 450, 670);
    EndPage(hPrinterDC);
    StartPage(hPrinterDC);
    memDC = CreateCompatibleDC(hPrinterDC);
    SelectObject(memDC, hBitmap);
    BitBlt(hPrinterDC, 250, 450, bm.bmWidth, bm.bmHeight, memDC, 0, 0, SRCCOPY);
    DeleteDC(memDC);
    EndPage(hPrinterDC);
    EndDoc(hPrinterDC);
    DeleteDC(hPrinterDC);
    DeleteObject(hBitmap);

Ejemplo 78

Para poder compilar este ejemplo hay que incluir entre las librerías "winspool.lib".

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 78 win078.zip 2012-04-18 258437 bytes 672

Usando PrintDlg

Windows dispone de un cuadro de diálogo común para imprimir documentos. Usando ese cuadro podemos obtener del usuario todos los parámetros necesarios para configurar la impresión: impresora, páginas a imprimir, número de copias e intercalado de copias.

Diálogo de impresión
Diálogo de impresión

Para usarlo debemos invocar a la función PrintDlg usando como parámetro un puntero a una estructura PRINTDLG. Previamente deberemos inicializar esa estructura con los datos adecuados para nuestro documento.

    PRINTDLG pd = { sizeof(PRINTDLG),
        hwnd, 0, 0,
        0, // hDC
        PD_ALLPAGES | PD_RETURNDC | PD_USEDEVMODECOPIESANDCOLLATE,
        1, 2, // Desde la página 1 a la 2
        1, 2, // La primera página es la 1, la última la 2
        1,  // Copias
        NULL, 0, 0, 0, 0, 0, 0, 0};

Los valores que se suelen iniciar en la estructura son:

  • El primer parámetro es el tamaño de la estructura PRINTDLG. Se debe indicar este tamaño ya que en distintas versiones del API la estructura puede haber añadido distintos campos.
  • El segundo es un manipulador de la ventana padre del cuadro de diálogo.
  • Los dos valores siguientes se usan para indicar la impresora. Si se inician a NULL (o 0), el cuadro de diálogo se encarga de inicializar esos valores con las impresora por defecto. Nosotros usaremos ceros. Al regresar, estos campos tienen manipuladores de memoria para objetos DEVMODE y DEVNAMES que nos indican la impresora seleccionada. Nosotros deberemos liberar esa memoria cuando ya no la necesitemos. La estructura DEVMODE nos puede resultar útil para tener en cuenta las características de la impresora, dimensiones, si es color o blanco y negro, etc.
  • El siguiente campo es un manipulador de DC. Si se usa el flag PD_RETURNDC, al retornar este campo tendrá un DC para la impresora seleccionada. En realidad, esto es lo que necesitamos para imprimir. Igual que siempre, la aplicación debe liberar este DC cuando ya no lo necesite, mediante una llamada a DeleteDC.
  • Las banderas, para indicar que queremos que se devuelva un DC para la impresora (PD_RETURDC), que inicialmente se seleccionan todas las páginas (PD_ALLPAGES), y el valor PD_USEDEVMODECOPIESANDCOLLATE, que indica que la opción de múltiples copias estará inhibido si la impresora no soporta esa opción.
  • Los dos valores siguientes indican el rango de páginas seleccionadas. Cuando el usuario active la opción de "selección" se imprimirá ese rango de páginas.
  • Los dos siguientes indican el número de la primera y última paginas. Estos valores se usan para verificar si el rango introducido por el usuario es o no válido.
  • El siguiente valor indica el número de copias, si la impresora seleccionada soporta varias copias, si no, se ignora. Al retornar indica el número de copias pedidas por el usuario.
  • El resto de los valores no nos interesan de momento, ya que usaremos el recurso por defecto.
              case CM_IMPRIMIR:
                if(PrintDlg(&pd)) {
                    ImprimirDocumento(&pd);
                    GlobalFree(pd.hDevMode);
                    GlobalFree(pd.hDevNames);
                    DeleteDC(pd.hDC);
                }
                break;

Es importante liberar el manipulador del DC creado por PrintDlg y los manipuladores de memoria asignados para las estructiras DEVNAMES y DEVMODE.

Ejemplo 79

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 79 win079.zip 2012-06-15 258820 bytes 702