Categoría: API de Windows

Conectar y desconectar unidades de red

Unidad de redAlgunas veces nos encontramos con la necesidad de que nuestra aplicación acceda a archivos almacenados en un ordenador diferente, aunque conectado a la misma red.

En esta entrada veremos cómo conectarse y cómo desconectarse a una unidad de red, usando el API de Windows. Mientras exista esa conexión, podremos acceder a ficheros en la unidad remota del mismo modo que a cualquier fichero local.

Básicamente usaremos dos funciones del API de Windows: WNetAddConnection2 y WNetCancelConnection2, para crear y eliminar una conexión, respectivamente.

Estas funciones están en la librería de Windows «Mpr«, por lo tanto, tendremos que incluirla en la fase de enlazado de nuestro programa.

Para el programa de ejemplo usaremos una carpeta en una unidad de disco local, por ejemplo, C:\temp. Aunque si estamos en una red podremos conectar a cualquier recurso compartido de otros ordenadores, siempre que nuestro usuario tenga los permisos adecuados.

Antes de poder crear una unidad de red deberemos compartir ese recurso en la red, para ello usaremos el explorador de archivos de Windows, y accederemos a las propiedades de la carpeta que queramos compartir. Vamos a la pestaña de «Compartir», y pulsamos el botón de «Compartir». En el siguiente cuadro de diálogo nos permitirá elegir con qué usuarios queremos compartir el recurso, y una vez elegidos, nos mostrará los datos relativos a ese recurso. En nuestro ejemplo algo como:

temp (\\<etiqueta>)
\\<etiqueta>\temp

Donde <etiqueta> es el nombre de nuestro ordenador.

Este nombre se puede sustituir por la IP, en nuestro ejemplo»127.0.0.1″, o por el nombre que tenga asignado en el DNS local, por ejemplo «localhost».

En general, la etiqueta corresponderá al nombre de un equipo conectado a la red, y el nombre de la unidad o carpeta compartida será diferente.

Las funciones para conectarse y desconectarse a una unidad remota tendrán esta forma, más o menos:

bool ConectarRecurso()
{
    NETRESOURCE nr;
    DWORD ret;
    const char *letra = "Z:";
    const char *recurso = "\\\\localhost\\temp";
    const char *usuario = "usuario";
    const char *password = "contraseña";
    bool conexionRemota;

    nr.dwType = RESOURCETYPE_ANY;
    nr.lpLocalName = (LPSTR)letra;
    nr.lpRemoteName = (LPSTR)recurso;
    nr.lpProvider = NULL;
    ret = WNetAddConnection2(&nr, password, usuario, 0);

    if(ret !=NO_ERROR)
        switch(ret) {
            case ERROR_ACCESS_DENIED: 
               std::printf("Access to the network resource was denied."); 
               break;
            case ERROR_ALREADY_ASSIGNED: 
               std::printf("The local device specified by lpLocalName is already connected to a network resource."); 
               break;
            case ERROR_BAD_DEV_TYPE: 
               std::printf("The type of local device and the type of network resource do not match."); 
               break;
            case ERROR_BAD_DEVICE: 
               std::printf("The value specified by lpLocalName is invalid."); 
               break;
            case ERROR_BAD_NET_NAME: 
               std::printf("The value specified by lpRemoteName is not acceptable to any network resource provider. The resource name is invalid, or the named resource cannot be located."); 
               break;
            case ERROR_BAD_PROFILE: 
               std::printf("The user profile is in an incorrect format."); 
               break;
            case ERROR_BAD_PROVIDER: 
               std::printf("The value specified by lpProvider does not match any provider."); 
               break;
            case ERROR_BAD_USERNAME: 
               std::printf("The specified user name is not valid."); 
               break;
            case ERROR_BUSY: 
               std::printf("The router or provider is busy, possibly initializing. The caller should retry."); 
               break;
            case ERROR_CANCELLED: 
               std::printf("The attempt to make the connection was cancelled by the user through a dialog box from one of the network resource providers, or by a called resource."); 
               break;
            case ERROR_CANNOT_OPEN_PROFILE: 
               std::printf("The system is unable to open the user profile to process persistent connections."); 
               break;
            case ERROR_DEVICE_ALREADY_REMEMBERED: 
               std::printf("An entry for the device specified in lpLocalName is already in the user profile."); 
               break;
            case ERROR_EXTENDED_ERROR: 
               std::printf("A network-specific error occured. Call the WNetGetLastError function to get a description of the error."); 
               break;
            case ERROR_INVALID_ADDRESS: 
               std::printf("An attempt was made to access an invalid address.");
               break;
            case ERROR_INVALID_PARAMETER: 
               std::printf("A parameter is incorrect.");
               break;
            case ERROR_INVALID_PASSWORD: 
               std::printf("The specified password is invalid."); 
               break;
            case ERROR_LOGON_FAILURE: 
               std::printf("A logon failure because of an unknown user name or a bad password."); 
               break;
            case ERROR_NO_NET_OR_BAD_PATH: 
               std::printf("A network component has not started, or the specified name could not be handled."); 
               break;
            case ERROR_NO_NETWORK: 
               std::printf("There is no network present."); 
               break;
            default: 
               std::printf("Error %d, desconocido", ret); 
               break;
        }
    conexionRemota = ((ret == NO_ERROR) || (ret == ERROR_ALREADY_ASSIGNED));
    return conexionRemota;
}

void DesconectarRecurso()
{
    const char *letra = "Z:";

    WNetCancelConnection2(letra, 0, TRUE);
}

Habría que sustituir «usuario» y «contraseña» por los valores adecuados en cada caso.

Se puede descargar un ejemplo completo desde aquí.

Acentos y eñes en programas de consola de Windows

Letra Ñ Cuando creamos programas para consola de Windows hay algo que resulta muy frustrante: las eñes, cedillas y acentos no se muestran como debieran, y en su lugar se ven unos caracteres raros que dificultan la lectura de los textos y resultan muy antiestéticos.

Ante este problema se nos ocurren algunas soluciones, más o menos ingeniosas:

La primera, prescindir de esos caracteres: eliminamos los acentos y evitamos palabras con ‘ñ’. Esto no siempre es posible, y como solución, sinceramente, resulta un intento bastante mediocre.

La segunda consiste en editar el código fuente desde un editor de consola. Haciendo esto, el editor usa el mismo conjunto de caracteres que el fichero ejecutable, y la salida del programa coincide con lo que hemos escrito antes. El resultado es mejor, pero resulta bastante incómodo usar varios editores para cada proyecto.

La tercera es incluso más incómoda. Consiste en escribir las cadenas en una consola, y a continuación cortar y pegar el texto en el editor. Y lo peor, a veces este truco ni siquiera funciona.

Pero, ¿por qué pasa esto?

Tal vez nos resultaría más sencillo solucionar el problema si supiéramos exactamente por qué se produce.

La raíz del problema es que Windows usa un código de caracteres (o código de página) diferente para el entorno GUI y para la consola. Para configuraciones de Windows en español, en GUI se usa el código 1252 y para la consola se usa el código 850.

Si usamos un editor de textos GUI para escribir el código de un programa para consola, se usarán códigos de página diferentes en la edición y en la ejecución, y el resultado será el galimatías al que ya estamos acostumbrados.

Soluciones más interesantes

Conociendo el origen del problema es más sencillo encontrar una solución, o al menos, debería serlo.

Una primera idea sería configurar el editor para que use el código de página 850. Desgraciadamente esto no es siempre posible. En las opciones del editor de Code::Blocks, por ejemplo, no está disponible el código de página 850.

Eso nos deja la solución final, que consiste en usar un par de funciones del API de Windows.

Primero, usaremos la función SetConsoleOutputCP. Que, como su propio nombre indica, sirve para asignar un código de página a la salida de consola. Usaremos el valor 1252, que es el código de página que queremos usar, ya que es el que hemos usado para escribir el código fuente.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    SetConsoleOutputCP(1252);
    cout << "áéíóúñÑçÇüÜ" << endl;
    return 0;
}

Segundo, debemos asegurarnos de que nuestra consola usa fuentes Unicode, de otro modo, el código de página asignado será ignorado y no habremos conseguido nada. En la página de Microsoft hay un artículo titulado «SetConsoleOutputCP sólo es efectivo con fuentes Unicode«:

Hacer esto es sencillo, basta seguir estos pasos:

  1. Abrir una consola Windows.
  2. Desplegar el menú del sistema (en el icono de la parte superior izquierda de la ventana) y seleccionar «Predeterminados».
  3. Activar la pestaña «Fuentes».
  4. Seleccionar una fuente TrueType, si está disponible, en lugar de «Fuentes de mapa de bits». (Yo tengo «Lucida console».)

De este modo, la configuración será la usada para todas las consolas a partir de este momento.

Si sólo están disponibles fuentes de mapas de bits, mucho me temo que esta solución no es viable.

Efectos colaterales

Si nuestro programa de consola tiene que leer cadenas nos encontraremos con otro problema inesperado. Las cadenas que introduzcamos por teclado, si contienen acentos o eñes se verán correctamente, pero si posteriormente las volvemos a mostrar en pantalla veremos que vuelven a aparecer caracteres extraños.

El motivo es simple, el código de página para consola de entrada y salida no tiene por qué ser el mismo. La función SetConsoleOutputCP sólo cambia el código de página para la salida, pero el de entrada sigue siendo el 850. Así, cuando introducimos una cadena, esta se codifica incorrectamente.

La función para cambiar el código de página de entrada no es, como podría esperarse, SetConsoleInputCP. Esa función no existe (a saber por qué), la función correcta es SetConsoleCP.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    char cad[256];

    SetConsoleOutputCP(1252);
    SetConsoleCP(1252);
    cout << "áéíóúñÑçÇçüÜ" << endl;
    cout << "Introduce una cadena con acentos y eñes: ";
    cin >> cad;
    cout << "La cadena es: " << cad;
    return 0;
}

No es una solución ideal, lo sé, pero puede solucionar muchos problemas cuando programamos para consola.

Otra ventaja es que las cadenas leídas de este modo se codifican internamente usando el código de página de Windows GUI, de modo que es una forma de que cualquier dato leído de este modo se visualice correctamente en una aplicación GUI.

Impedir que una aplicación se ejecute dos veces

Una direccion¿Cómo podemos evitar que una aplicación se ejecute más de una vez de forma simultánea?

En Windows existe un método sencillo: usar un mutex.

El nombre mutex es una abreviatura de un algoritmo de exclusión mutua. Es un dispositivo que se usa normalmente para evitar concurrencia, es decir para controlar el acceso de determinadas zonas de código que tienen acceso a recursos compartidos.

En nuestro caso, el recurso al que tenemos que limitar el acceso es el ordenador. Como se trata de un recurso que puede ser compartido, sólo limitaremos el acceso a nuestro programa, es decir, cuando nuestro programa esté accediendo al sistema operativo impedirá que otras instancias del mismo programa puedan acceder a él.

Un mutex es ideal para este propósito, ya que sólo puede estar en posesión de un proceso a la vez. Si un proceso tiene un manipulador de un mutex, no es posible que otro pueda obtenerlo.

De todos modos, para el propósito de este artículo no usaremos el mutex de ese modo, sencillamente intentaremos crear un mutex con un nombre determinado. Si el mutex no existía previamente, continuamos con la ejecución de la aplicación, si existía, abandonamos la ejecución.

Para obtener un manipulador para un mutex usaremos la función del API de Windows CreateMutex. Los dos primeros parámetros no nos importan demasiado. El primero es un descriptor de seguridad, que para este propósito nos basta con el descriptor por defecto. El segundo sirve para obtener la propiedad del mutex cuando es creado o no. Para nuestro caso es indiferente, ya que sólo verificaremos si el mutex existe o no. El tercero es el nombre del mutex. Usaremos un nombre único para nuestra aplicación.

Hay que tener presente que lo que impedirá que nuestra aplicación se ejecute es la presencia del mutex, de modo que este mecanismo no sólo impide que dos instancias del mismo programa se ejecuten a la vez, también impedirá la ejecución de cualquier otra aplicación que use el mismo nombre para el mutex.

// Prueba de mutex
// Mayo 2013 Con Clase
// Salvador Pozo
#include <windows.h>
#include <iostream>

using namespace std;

int main()
{
    bool EnEjecucion;

    //mutex =
    CreateMutex(NULL, FALSE, "PROGRAMA_EN_EJECUCION");
    EnEjecucion = (GetLastError() == ERROR_ALREADY_EXISTS || GetLastError() == ERROR_ACCESS_DENIED);

    if(EnEjecucion) {
        MessageBox(NULL, "El programa ya está en ejecución", "Probar mutex", MB_OK);
        return 0;
    }
    // Resto del programa aquí...
    cin.get();
    return 0;
}

Alternativamente a  usar mutex podemos hacer lo mismo con un fichero de disco. Si no existe, lo creamos al iniciar la aplicación y lo borramos al terminar. Si existe, cerramos la aplicación.

El problema de este método es que si la aplicación termina de forma imprevista el fichero no se borra, y no se podrá ejecutar la aplicación hasta que borremos el fichero de forma manual.

La idea es siempre crear un objeto global desde el punto de vista del sistema operativo, que sólo exista mientras la aplicación esté en ejecución. La ventaja del mutex es que se destruye automáticamente cuando la aplicación termina, independientemente del modo en que termine, aunque sea a causa de un error.

Colocar un icono en el área de notificación

IconoA veces puede ser interesante que nuestras aplicaciones coloquen un icono en el área de notificación.

Sin embargo, tampoco conviene abusar de esta técnica. El área de notificación se diseñó para proporcionar mensajes importantes al usuario, pero si se añaden demasiados iconos esta funcionalidad puede perder gran parte de su eficacia.

Las aplicaciones candidatas para colocar iconos en la zona de notificación son aquellas que no necesitan mantener un interfaz con el usuario, y que se ejecutan en segundo plano. El icono indica al usuario que el programa se está ejecutando. Cambiando el icono, la aplicación puede enviar mensajes cuando se requiera la atención del usuario, ya sea mediante un texto, o modificando la apariencia del icono. Además, sirve como punto para que el usuario acceda a un menú contextual, o para que maximice la ventana asociada.

La información que se encuentra por Internet sobre este tema es abundante, pero casi nadie explica cómo hacerlo usando sólo el API de Windows. Siempre he pensado que sería difícil de hacer, pero la verdad es que es muy fácil, así que paso a explicarlo.

Funciones, estructuras y mensajes del API

Lo he puesto en plural, pero lo cierto es que sólo nos interesa una estructura de datos, una función del API y tres mensajes.

La estructura es NOTIFYICONDATA, que actualmente tiene esta definición:

typedef struct _NOTIFYICONDATA {
  DWORD cbSize;
  HWND  hWnd;
  UINT  uID;
  UINT  uFlags;
  UINT  uCallbackMessage;
  HICON hIcon;
  TCHAR szTip[64];
  DWORD dwState;
  DWORD dwStateMask;
  TCHAR szInfo[256];
  union {
    UINT uTimeout;
    UINT uVersion;
  };
  TCHAR szInfoTitle[64];
  DWORD dwInfoFlags;
  GUID  guidItem;
  HICON hBalloonIcon;
} NOTIFYICONDATA, *PNOTIFYICONDATA;

En versiones anteriores a Windows 2000, los miembros desde dwState en adelante no existían, y se han ido añadiendo en sucesivas versiones del API. Para un uso básico no necesitaremos esos datos miembro.

Y la función es Shell_NotifyIcon, que tiene este prototipo:

WINSHELLAPI BOOL WINAPI Shell_NotifyIcon(
    DWORD dwMessage,    // message identifier
    PNOTIFYICONDATA pnid    // pointer to structure
);

Hay, además, tres mensajes:

NIM_ADD
NIM_DELETE
NIM_MODIFY

Y en las últimas versiones del API se han añadido dos mensajes más:

NIM_SETFOCUS
NIM_SETVERSION

De todos modos, no hablaré de ellos aquí, porque no los vamos a necesitar.

En una entrada futura hablaremos de otras posibilidades del área de notificación, como mensajes emergentes en forma de globo.

Añadir un icono

Usaremos la función Shell_NotifyIcon para enviar un mensaje NIM_ADD, que insertará un icono en el área de notificación.

Previamente rellenaremos una estructura NOTIFYICONDATA con los valores adecuados.

#define IDENTIFICADOR 3200
#define MENSAJE WM_USER+11
void MostrarIcono(HWND hwnd) {
    HICON hIcon = LoadIcon(GetModuleHandle(0), "Icono");
    NOTIFYICONDATA nid;

    nid.cbSize = sizeof(nid);
    nid.hWnd = hwnd;
    nid.uID = IDENTIFICADOR;
    nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    nid.uCallbackMessage = MENSAJE;
    nid.hIcon = hIcon;
    strcpy(nid.szTip, "Estoy en el área de notificación!");
    Shell_NotifyIcon(NIM_ADD, &nid);
    DestroyIcon(hIcon);
}

El miembro cbSize debe contener el tamaño de la estructura. En general, las estructuras que tienen un miembro con este nombre es porque son diferentes en cada versión de Windows, o al menos está previsto que su estructura cambie. Este es el caso, ya que esta estructura ha crecido desde Windows 2000, XP y Vista.

El miembro hWnd debe contener un manipulador de la ventana de la aplicación asociada al icono.

El miembro uID contiene un identificador de la pequeña ventana dentro del área de notificación, asociada al icono. Se usa para enviar mensajes procedentes del icono relacionados con el ratón.

uFlags contiene una combinación de banderas que indica qué campos de la estructura contienen valores válidos. En este caso, NIF_ICON indica que hIcon contiene un manipulador de icono, NIF_MENSAJE indica que uCallbackMessage contiene un código de mensaje válido, y NIF_TIP que szTip contiene una cadena para un tooltip asociado al icono. Este texto se mostrará cuando el ratón se sitúe en el icono.

En uCallbackMessage almacenaremos un valor entero que será usado como identificador de mensaje. Este mensaje se enviará a la ventana asociada, que hemos indicado en el campo hWnd. En wParam se enviará el valor de uID y en lParam el mensaje detectado, por ejemplo WM_MOUSEMOVE o WM_LBUTTONDOWN.

Por si no lo has notado, la misma aplicación puede poner varios iconos diferentes en el área de notificación. Los mensajes procedentes de cada uno de ellos se distinguen por el valor de uID.

En hIcon colocaremos el manipulador del icono que queremos mostrar. Es importante destruir el manipulador después de llamar a la función Shell_NotifyIcon, ya que de no hacerlo se pueden producir fugas de memoria.

Por último, en szTip copiaremos la cadena a usar como texto en el tooltip asociado al icono. Esta cadena está limitada a 64 caracteres.

Modificar el icono

Podemos cambiar el icono cuando queramos.

Por ejemplo, supongamos que el icono que se muestra cuando la aplicación se está ejecutando correctamente, y no necesita atención por parte del usuario es Icono1. En un momento determinado se produce una situación que queremos notificar. En ese caso, procedemos a cambiar el icono por Icono2. Podemos modificar también el texto del tooltip, para dar una pista sobre la nueva situación.

void CambiarIcono(HWND hwnd, HICON hIcon) {
    NOTIFYICONDATA nid;

    nid.cbSize = sizeof(nid);
    nid.hWnd = hwnd;
    nid.uID = IDENTIFICADOR;
    nid.uFlags = NIF_ICON;
    nid.hIcon = hIcon;
    Shell_NotifyIcon(NIM_MODIFY, &nid);
    DestroyIcon(hIcon);
}

En este caso usaremos el mensaje NIM_MODIFY, y como sólo queremos cambiar el icono, uFlags contendrá el valor NIF_ICON.

Por supuesto, en hIcon colocaremos el manipulador del nuevo icono a mostrar.

Modificar el texto

De un modo similar, podemos modificar el texto del tooltip.

void CambiarTexto(HWND hwnd, char *cad) {
    NOTIFYICONDATA nid;

    nid.cbSize = sizeof(nid);
    nid.hWnd = hwnd;
    nid.uID = IDENTIFICADOR;
    nid.uFlags = NIF_TIP;
    strcpy(nid.szTip, cad);
    Shell_NotifyIcon(NIM_MODIFY, &nid);
}

Ahora uFlags debe valer NIF_TIP, y deberemos modificar el valor de szTip.

Quitar icono de área de notificación

Para eliminar un icono usaremos el mensaje NIM_DELETE

void OcultarIcono(HWND hwnd) {
    NOTIFYICONDATA nid;

    nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.hWnd = hwnd;
    nid.uID = IDENTIFICADOR;

    Shell_NotifyIcon(NIM_DELETE, &nid);
}

No necesitamos especificar uFlags, ni ninguno de los valores opcionales.

Responder a mensajes desde el área de notificación

Si hemos indicado el valor NIF_MESSAGE al añadir el icono, el procedimiento de ventana asociado recibirá un mensaje MENSAJE. En wParam recibiremos el identificador asociado al icono, y en lParam el mensaje original.

Para procesar estos mensajes sólo hay que añadir el caso adecuado para MENSAJE.

switch (message) { /* handle the messages */
...
    case MENSAJE:
        if(wParam == IDENTIFICADOR) {
            switch(lParam) {
                 case WM_LBUTTONUP:
                     OcultarIcono(hwnd);
                     ShowWindow (hwnd, SW_RESTORE);
                     break;
            }
        }
        break;
...

Este ejemplo restaurará la ventana asociada al icono cuando se detecte el que el botón izquierdo del ratón fue soltado sobre el icono del área de notificación. Por supuesto, podemos procesar todos los mensajes que se reciban desde el icono, como WM_MOUSEMOVE, WM_LBUTTONDOWN, etc.

Ejemplo completo

Puedes descargar un ejemplo completo desde aquí.