Tutorial WINAPI C++ 2.4 (Crear ObjetoEscena_Records)
Categorías : Windows, Programación, C y C++.

En este tutorial vamos a tener que usar lo que hemos visto sobre archivos y directorios para crear un objeto que sea capaz de guardar nuestas puntuaciones maximas en el disco duro.
Vamos a tener que usar el objeto ObjetoEscena_VentanaTranslucida para hacer el ultimo modelo de ventana translucida que nos faltara para tener listo el juego, y ademas vamos a simular un cursor al estilo ms-dos para introducir el nombre de la puntuacion.
Para este objeto nos vamos a tener que complicar algo mas que con los 3 últimos basados en ObjetoEscena_VentanaTranslucida.
Necesitamos es tener un objeto que cargue y guarde los records en un archivo además de mostrarlos en la escena. También debemos pensar que este objeto deberá ser capaz de obtener los caracteres pulsados del teclado, para poder guardar el nombre del record.
Lo primero será definir una clase para almacenar los datos de un record, veamos la declaración :
// Clase que contiene los datos de una puntuación del juegoclass Record_Snake {public: // Miembros publicos// -ConstructorRecord_Snake(void) {Nombre = NULL;TamNombre = 0;};// -Destructor~Record_Snake(void) {if (Nombre != NULL) delete Nombre;};// -PuntosUINT Puntos;// -Total de cuadros recorridosUINT Recorrido;// -NivelUINT Nivel;// -Nombre del recordTCHAR *Nombre;// -Tamaño en caracteres del nombreUINT TamNombre;};
Como podemos observar tenemos una serie de variables para almacenar los datos del record. Lo único que hay que remarcar es que el string que guardara el nombre será dinamico y se creara ajustado a los caracteres que contenga.
Ahora que ya tenemos en mente los datos que necesitamos para un record podemos crear el ObjetoEscena_Records, veamos su declaración :
// Clase que encapsula una ventana translucida que sirve para ver los records dentro de un ObjetoEscenaclass ObjetoEscena_Records : public ObjetoEscena_VentanaTranslucida {public : ////////////////////// Miembros publicos// -ConstructorObjetoEscena_Records(void);// -Destructor~ObjetoEscena_Records(void);// -Función que carga el archivo de recordsvoid CargarRecords(void);// -Función que guarda el archivo de recordsvoid GuardarRecords(void);// -Función que agrega un caracter al nuevo nombre del record actualvoid Agregar_Caracter(const TCHAR Tecla);// -Función que se llama al final de pintar todos los graficos// (Los graficos pintados no seran modificados con AlphaBlend).virtual void Pintar_Terminado(HDC hDCDestino);// -Función que calcula el tamaño y hace visible la ventana del mensajeBOOL MostrarRecords(const UINT nPuntos, const UINT nRecorrido, const UINT nNivel);// -Función que oculta la ventana de los recordsvoid OcultarRecords(void);// -Función que retorna si se esta introduciendo un nuevo recordinline BOOL IntroduciendoNuevoRecord(void) const { return IntroduciendoRecord; };private: ////////////////////// Miembros privados// -Vector que contiene los records del juegostd::vector<Record_Snake *> Records;// -Posición del nuevo recordUINT PosNuevoRecord;// -Valor que determina si se esta introduciendo un recordBOOL IntroduciendoRecord;// -Milisegundos para hacer parpadear el cursorDWORD MSParpadeo;// -Valor que determina si el cursor sera visible o nobool Parpadeo;};
Podemos ver que hay una función para cargar los records, una para guardarlos, una para agregar caracteres (la cual se usara a la hora de escribir el nuevo record), una función Pintar_Terminado que usaremos para pintar los records, una función MostrarRecords que calculara la posición y mostrara la ventana de los records, una función OcultarRecords para ocultar la ventana y por ultimo una función IntroduciendoNuevoRecord que nos servirá para saber si se está introduciendo un nuevo record o no.
Veamos la función CargarRecords :
// Función que carga los records del juegovoid ObjetoEscena_Records::CargarRecords(void) {// Borramos los records de la memoriasize_t i = 0;for (i = 0; i < Records.size(); i++) delete Records[i];Records.resize(0);// Declaración de variables y objetosObjetoDirectoriosWindows Directorios;ObjetoArchivo ArchivoRecords;TCHAR RutaAppData[MAX_PATH]; // DirectorioTCHAR RutaAppDataTxt[MAX_PATH]; // Archivo de prueba// Obtengo el directorio AppDataDirectorios.AppData(RutaAppData);// Añado el resto de la rutawcscat_s(RutaAppData, MAX_PATH, TEXT("\\Tutoriales www.devildrey33.es"));// Creo un string con el nombre del archivo con el que se va a trabajarwcscpy_s(RutaAppDataTxt, MAX_PATH, RutaAppData);wcscat_s(RutaAppDataTxt, MAX_PATH, TEXT("\\Records.snake"));Record_Snake *Tmp;if (ArchivoRecords.AbrirArchivo(RutaAppDataTxt, false) == TRUE) {for (i = 0; i < 10; i++) {Tmp = new Record_Snake;Records.push_back(Tmp);ArchivoRecords.LeerUINT(Tmp->Puntos);ArchivoRecords.LeerUINT(Tmp->Recorrido);ArchivoRecords.LeerUINT(Tmp->Nivel);Tmp->TamNombre = ArchivoRecords.LeerString(Tmp->Nombre);}ArchivoRecords.CerrarArchivo();}// Si no existe el archivo de records creo una tabla con 10 records a ceroelse {for (i = 0; i < 10; i++) {Tmp = new Record_Snake;Tmp->Puntos = 0;Tmp->Recorrido = 0;Tmp->Nivel = 0;Tmp->Nombre = NULL;Tmp->TamNombre = 0;Records.push_back(Tmp);}}}
La función CargarRecords lo primero que hace es obtener la ruta de trabajo mediante la función AppData del ObjetoDirectoriosWindows, luego le añade la parte final del directorio “\Tutoriales www.devildrey33.es” con la función wcscat_s. Una vez tenemos determinada la ruta de trabajo creamos otro string con dicha ruta mas el nombre del archivo, para ello utilizamos wcscpy_s y wcscat_s (que son las funciones Unicode seguras de strcopy y strcat). Cuando tenemos la ruta del archivo intentamos abrirlo y leer sus datos, en caso de que el archivo no exista crearemos un vector de 10 records con todo a cero.
Viendo la función CargarRecords creo que podemos omitir la explicación para la función GuardarRecords que en esencia hace algo similar, asi que pasemos a ver la función Pintar_Terminado :
// Esta función se usara para pintar los graficos que se necesiten. (NO SE INCLUIRAN EN EL ALPHABLEND)// NOTA el hDCDestino es un backbuffer creado por la funcion Pintar, por ello no hace falta crear otro buffer de pintado.void ObjetoEscena_Records::Pintar_Terminado(HDC hDCDestino) {RECT EspacioCursor;SIZE TamTexto;// Fuente negritaHFONT Fuente = CreateFont( 16, 0, 0, 0, FW_BOLD, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );// Fuente normalHFONT Fuente2 = CreateFont( 16, 0, 0, 0, FW_NORMAL, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );// Fuente inicialmente seleccionada en el DCHFONT VFuente = static_cast<HFONT>(SelectObject(hDCDestino, Fuente));// Calculamos el cambio de parpadeoif (MSParpadeo < GetTickCount()) {Parpadeo = !Parpadeo;MSParpadeo = GetTickCount() + 300;}TCHAR TmpTxt[256];UINT TmpTam = 0;UINT AnchoNombre = 0;// Pintamos la cabeceraSetBkMode(hDCDestino, TRANSPARENT);SetTextColor(hDCDestino, RGB(0, 0, 0));TextOut(hDCDestino, 11, 11, TEXT("Puntos"), 6);TextOut(hDCDestino, 66, 11, TEXT("Recorrido"), 9);TextOut(hDCDestino, 139, 11, TEXT("Nivel"), 5);TextOut(hDCDestino, 180, 11, TEXT("Nombre"), 6);SetTextColor(hDCDestino, RGB(255, 255, 255));TextOut(hDCDestino, 10, 10, TEXT("Puntos"), 6);TextOut(hDCDestino, 65, 10, TEXT("Recorrido"), 9);TextOut(hDCDestino, 138, 10, TEXT("Nivel"), 5);TextOut(hDCDestino, 179, 10, TEXT("Nombre"), 6);SelectObject(hDCDestino, Fuente2);// Pintamos los valores de los recordsfor (size_t i = 0; i < Records.size(); i++) {// Pintamos la sombra del textoSetTextColor(hDCDestino, RGB(0, 0, 0));TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Puntos);TextOut(hDCDestino, 11, (15 * i) + 26, TmpTxt, TmpTam);TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Recorrido);TextOut(hDCDestino, 66, (15 * i) + 26, TmpTxt, TmpTam);TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Nivel);TextOut(hDCDestino, 139, (15 * i) + 26, TmpTxt, TmpTam);TextOut(hDCDestino, 180, (15 * i) + 26, Records[i]->Nombre, Records[i]->TamNombre);if (i != PosNuevoRecord) { // Si no es la posición del record actual lo pintamos blancoSetTextColor(hDCDestino, RGB(255, 255, 255));}else { // Si es la posición del record actual// Si el parpadeo esta activado y estamos introduciendo un record, pintamos el cursorif (Parpadeo == true && IntroduciendoRecord == TRUE) {TamTexto.cx = 0;if (Records[i]->Nombre != NULL)GetTextExtentPoint32(hDCDestino, Records[i]->Nombre, Records[i]->TamNombre -1, &TamTexto);EspacioCursor.left = 180 + TamTexto.cx;EspacioCursor.right = EspacioCursor.left + 2;EspacioCursor.top = (15 * i) + 25;EspacioCursor.bottom = EspacioCursor.top + 15;FillRect(hDCDestino, &EspacioCursor, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));}SetTextColor(hDCDestino, RGB(0, 255, 255));}// Pintamos el textoTmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Puntos);TextOut(hDCDestino, 10, (15 * i) + 25, TmpTxt, TmpTam);TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Recorrido);TextOut(hDCDestino, 65, (15 * i) + 25, TmpTxt, TmpTam);TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Nivel);TextOut(hDCDestino, 138, (15 * i) + 25, TmpTxt, TmpTam);TextOut(hDCDestino, 179, (15 * i) + 25, Records[i]->Nombre, Records[i]->TamNombre);}SelectObject(hDCDestino, Fuente);// Pintamos un texto o otro segun si se esta introduciendo o mirando un record.if (IntroduciendoRecord == TRUE) {SetTextColor(hDCDestino, RGB(0, 0, 0));TextOut(hDCDestino, 11, 186, TEXT("Nuevo record!! Introduce tu nombre"), 40);SetTextColor(hDCDestino, RGB(240, 240, 0));TextOut(hDCDestino, 10, 185, TEXT("Nuevo record!! Introduce tu nombre"), 40);}else {SetTextColor(hDCDestino, RGB(0, 0, 0));TextOut(hDCDestino, 7, 186, TEXT("Presiona ESPACIO para volver a empezar"), 39);SetTextColor(hDCDestino, RGB(240, 240, 0));TextOut(hDCDestino, 6, 185, TEXT("Presiona ESPACIO para volver a empezar"), 39);}// Borramos objetos GDISelectObject(hDCDestino, VFuente);DeleteObject(Fuente);DeleteObject(Fuente2);}
Para empezar creamos un par de fuentes con la API CreateFont, una normal y una negrita. Luego miramos los milisegundos del parpadeo del cursor con la API GetTickCount, de forma que cada 300 milisegundos la variable Parpadeo cambie de true a false.
Lo siguiente es pintar los textos con sombra de la cabecera de los records, para ello utilizamos SetBkMode para establecer que los textos se pintaran sin fondo, SetTextColor para asignar el color del texto y TextOut para pintar el texto.
Despues creamos un bucle que recorra todos los records para pintarlos de uno en uno. Dentro del bucle utilizamos swprintf_s para formatear cada valor y los pintamos con TextOut. Ademas comprobamos si el record actual es el que se está pintando, para determinar si tenemos que pintar el cursor o no. Para pintar el cursor utilizamos FillRect y la brocha la obtenemos del stock de brochas de Windows con GetStockObject.
Por último pintamos el texto “Nuevo Record!!” si se está introduciendo un record, o en caso contrario pintamos el texto “Pulsa ESPACIO para volver a empezar”, y eliminamos de memoria todos los objetos GDI.
Ahora veamos la función MostrarRecords :
BOOL ObjetoEscena_Records::MostrarRecords(const UINT nPuntos, const UINT nRecorrido, const UINT nNivel) {// Cargo los records y miro si el nuevo record es mejor que alguno.IntroduciendoRecord = FALSE;CargarRecords();PosNuevoRecord = 10;size_t i = 0;for (i = 0; i < 10; i++) {// Si tiene mas puntos que este record es mejorif (nPuntos > Records[i]->Puntos) {PosNuevoRecord = i;break;}// Si tiene los mismos puntos miramos el recorridoelse if (nPuntos == Records[i]->Puntos) {// Si el recorrido es el mismo o mas pequeño, este record sera mejorif (nRecorrido <= Records[i]->Recorrido) {PosNuevoRecord = i;break;}}}// Si la posicion del nuevo record no es 10, es que hay que guardarlo// Ademas moveremos los records que sean inferiores una posicion en el arrayif (PosNuevoRecord != 10) {IntroduciendoRecord = TRUE;Record_Snake *Tmp = new Record_Snake;Tmp->Nivel = nNivel;Tmp->Puntos = nPuntos;Tmp->Recorrido = nRecorrido;delete Records[9];for (i = 9; i > PosNuevoRecord; i--) Records[i] = Records[i -1];Records[PosNuevoRecord] = Tmp;}// Calculo la posicion centrada dentro de la escenaRECT RectaEscena;GetClientRect(EscenaPadre->hWnd(), &RectaEscena);Espacio.left = (RectaEscena.right - 280) / 2;Espacio.top = (RectaEscena.bottom - 210) / 2;Espacio.right = Espacio.left + 280;Espacio.bottom = Espacio.top + 210;Visible = TRUE;ColorFondo = RGB(20, 120, 20);ColorBordeE = RGB(60, 160, 60);ColorBordeI = RGB(120, 220, 120);return static_cast<BOOL>(IntroduciendoRecord);}
El objetivo de esta función no es solo mostrar los records. Su finalidad también es ordenar los records de forma que se valore la puntuación por encima de todo, y luego si hay varias puntuaciones iguales que las ordene según la idea de que la que tenga menor recorrido es mejor.
El primer bucle determina si el record está entre los 10 primeros. Mas tarde partiendo de que sabemos la posición del record ordenamos los otros records de forma que borraremos el ultimo y añadiremos el nuevo en la posición que le toque. Por último se calcula la posición centrada para esta ventana y se pone el estado de visibilidad a TRUE.
Ya solo nos queda ver una función mas, Agragar_Caracter :
// -Función que agrega un caracter al nuevo nombre del record actualvoid ObjetoEscena_Records::Agregar_Caracter(const TCHAR Tecla) {TCHAR *TmpTxt = Records[PosNuevoRecord]->Nombre;switch (Tecla) {// Tecla Introcase VK_RETURN :IntroduciendoRecord = FALSE;return;// Tecla borrarcase VK_BACK :if (Records[PosNuevoRecord]->TamNombre > 0) Records[PosNuevoRecord]->TamNombre --;if (Records[PosNuevoRecord]->TamNombre == 0) {delete Records[PosNuevoRecord]->Nombre;Records[PosNuevoRecord]->Nombre = NULL;}else {Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre -1] = TEXT('\0');}return;// Las demas teclasdefault :if (Records[PosNuevoRecord]->Nombre == NULL) {Records[PosNuevoRecord]->Nombre = new TCHAR[2];Records[PosNuevoRecord]->Nombre[0] = Tecla;Records[PosNuevoRecord]->Nombre[1] = TEXT('\0');Records[PosNuevoRecord]->TamNombre = 2;}else {Records[PosNuevoRecord]->TamNombre ++;Records[PosNuevoRecord]->Nombre = new TCHAR[Records[PosNuevoRecord]->TamNombre];wcscpy_s(Records[PosNuevoRecord]->Nombre, Records[PosNuevoRecord]->TamNombre, TmpTxt);delete TmpTxt;Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre - 2] = Tecla;Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre - 1] = TEXT('\0');}return;}}
Esta función tiene por objetivo almacenar los caracteres introducidos por el teclado para formar una cadena que será el nombre del nuevo record.
Basicamente miramos si se ha presionado Intro para terminar con la introducción del nuevo record, si se ha pulsado la tecla borrar para borrar el ultimo carácter de la cadena, o si se ha pulsado cualquier otra tecla la cual añadiremos a la cadena.
En el ejemplo 2.4 podemos ver dentro de la escena una ventana que muestra los records, y que esta preparada para recibir pulsaciones de teclado y anotar el nombre del record.
Ya casi estamos al final del segundo tutorial, ahora nos las veremos con el objeto que cargara los niveles para el Snake: 2.5 - Tutorial Creación del ObjetoSnake_Nivel.
Descargar tutorial WinAPI completo | Snake compilada |