Tutorial: Crea un juego de Tres en Raya con Xojo

A continuación encontrarás traducido al castellano el artículo escrito por Gabriel Ludosanu y publicado originalmente en el Blog oficial de Xojo.

El juego de Tres en Raya es un juego de estrategia a dos jugadores donde cada uno de los jugadores sitúa por turnos su marca (una X o un O) en cada una de las cuadrículas de una rejilla de 3×3. El objeto es simple: ser el primero en alinear sus tres símbolos (X o 0) en cualquiera de las filas, columnas o diagonales de la rejilla.

Cuando finalices este tutorial habrás aprendido lo siguiente:

  • Crear un juego de escritorio con Xojo
  • Utilizar DesktopCanvas para el dibujado del juego
  • Implementar la lógica del juego y gestionar su estado
  • Gestionar las interacciones del usuario
  • Añadir efectos visuales y animaciones

Configurando el Proyecto

Ejecuta Xojo y Crea un Nuevo Proyecto

  • Abre el IDE de Xojo.
  • Selecciona el tipo de proyecto “Desktop”.
  • Define el Nombre de la Aplicación (por ejemplo, “TicTacToe”).
  • Haz clic en “Create”.

Crear el Control Personalizado TicTacToeGame

Con el objeto de contar con una estructura clara y modular del juego, la lógica del juego y la interfaz de usuario se implementarán en una subclase del control DesktopCanvas.

Estas son algunas de las ventajas clave a la hora de tomar este camino:

  • Encapsulación: Cubre toda la lógica del juego (como comprobar si se ha ganado o gestionar cada uno de los turnos de los jugadores), y también todos los elementos visuales (dibujar el tablero o animar los movimientos); todo ello en una única unidad autocontenida. Esto hace que el código sea más claro, fácil de comprender y más sencillo de mantener. Puedes reutilizar este control en otros proyectos sin tener que volver a escribir todo desde cero.
  • Organización: Un control personalizado permite una mejor organización del código al separar la funcionalidad del juego del resto del código de tu aplicación. Esto reduce la complejidad especialmente si tu aplicación se torna más grande e incorpora otras características.
  • Reusabilidad: Una vez que hayas creado el control TicTacToeGame podrás reutilizarlo en otros proyectos de Xojo. Sólo tendrás que arrastrar y soltarlo en una ventana.
  • Abstracción: El control personalizado proporciona una capa de abstracción. El resto de tu aplicación no tiene por qué saber cómo funciona internamente el juego TicTacToe, sólo necesita interactuar con la interface publicada por el control (como iniciar un nuevo juego o bien obtener la puntuación actual). Esto facilita modificar o actualizar la lógica del juego sin que se vean afectadas otras partes de la aplicación.

Estos son los pasos para crear este control personalizado basado en DesktopCanvas:

  1. Haz clic en el menú “Insert”.
  2. Selecciona “Class”.
  3. Nombra la clase como “TicTacToe”.
  4. Define la propiedad “Super” a “DesktopCanvas”.

Estructura de la clase TicTacToeGame

Comenzaremos definiendo algunas constantes y propiedades importantes que se utilizarán en la clase del juego.

Constantes

  • Private Const kBoardSize as Number = 3
    Define las dimensiones de la rejilla (3×3)
    Se utiliza para iterar las celdas del tablero
    Proporciona flexibilidad para potenciales cambios futuros en el tamaño de la rejilla.
  • Private Const kCellsPadding as Number = 40
    Controla el espacio en torno a los símbolos X y O
    Garantiza que los símbolos no toquen los bordes de la celda
    Proporciona un “respiro visual” en el dibujado de las celdas.

Propiedades

Propiedades del Estado del Juego
  • Public Property boardState(2,2) As Integer
    • Array 2D que representa el tablero.
    • Valores:
      • 0 = Celda vacía.
      • 1 = Jugador X.
      • 2 = Jugador O.
  • Public Property currentPlayer As Integer = 1
    • Registra el turno actual.
    • 1 = Jugador X.
    • 2 = Jugador O.
  • Public Property isGameOver As Boolean = False
    • Indica el estado del juego.
    • Evita futuros movimientos una vez que el juego ha finalizado.
Properties de Renderizado
Public Property CellHeight As Integer
Get
  Return Height / 3
End Get
 
Set
  
End Set
End Property
 
Public Property CellWidth As Integer
Get
  Return Width / 3
End Get
 
Set
  
End Set
End Property
  • CellHeight, CellWidth As Integer
    • Propiedades Computadas
    • Calcula de forma dinámica el tamaño de las celdas en función del tamaño del Canvas.
    • Divide el ancho/altura por tres para obtener celdas de igual tamaño en la rejilla.
  • ColorBoard, ColorX, ColorO As Color
    • Almacena los esquemas de color para los elementos del tablero.
    • Soporta los temas de modo oscuro/modo claro.
Propiedades de Animación
  • Public Property animationProgress As Double
    • Registra la animación del dibujado del símbolo.
    • Valores de 0 a 1.
    • Controla el escalado del símbolo durante su posición.
  • Public Property animationTimer As Timer
    • Gestiona el tiempo de la animación.
    • Dispara la animación suave del símbolo.
Registro de la Interacción
  • Public Property HoverCol As Integer = -1and Public Property HoverCol As Integer = -1
    • Registra la posición del ratón sobre la rejilla.
    • Activa el efecto sobre las celdas vacías.
    • Ofrece retroalimentación visual durante el juego.
Propiedades de Marcador
  • scoreX and scoreO As Integer
    • Registra el contador de victorias para cada jugador.
    • Se actualiza tras cada juego finalizado.

Las constantes y propiedades definidas anteriormente serán determinantes en los siguientes pasos, permitiéndonos la implementación de características clave: la encapsulación de la lógica del juego y también el renderizado, además de ofrecer una personalización flexible, interacciones y ajuste de tamaño dinámicas y ágiles, así como una interacción del usuario mejorada mediante el uso de animaciones y otros efectos.

Definición de Eventos

Para completar nuestra clase de juego, crearemos dos definiciones de eventos personalizadas, y que se utilizarán posteriormente durante el juego.

Evento GameStatus
Event GameStatus(info As String, playerTurn As String = "", scoreX As Integer, scoreO As Integer)
  • Propósito: Hace un seguimiento y comunica el estado actual del juego
  • Parámetros:
      • info: Una cadena que describe el estado actual del juego
      • playerTurn: Parámetro opcional indicando qué jugador tiene el turno
      • scoreX: Marcador actual para el Jugador X
      • scoreO: Marcador actual para el Jugador O
Evento GameOver
Event GameOver(result As String, scoreX As Integer, scoreO As Integer)
  • Propósito: Señaliza la finalización del juego con un vencedor o un empate
  • Parámetros:
    • result: Una cadena que describe el final del juego (por ejemplo, “X Wins”, “O Wins”, “Draw”)
    • scoreX: Marcador final para el Jugador X
    • scoreO: Marcador final para el Jugador O

Manejadores de Evento en el Juego Tic-Tac-Toe

Evento Closing
Sub Closing() Handles Closing
  // This event ensures that the animation timer is properly disabled and its handler is removed to prevent memory leaks or unexpected behavior.
  // Clean up the animation timer when the control is closing
  If animationTimer <> Nil Then
    animationTimer.Enabled = False
    RemoveHandler animationTimer.Action, AddressOf AnimationStep
    animationTimer = Nil
  End If
End Sub
  • Se encarga de limpiar los recursos utilizados
  • Desactiva y elimina el temporizador para evitar fugas de memoria
  • Invocado cuando se destruye el objeto
Evento MouseDown
Function MouseDown(x As Integer, y As Integer) Handles MouseDown as Boolean
  // This event handles mouse click actions on the game board.
  // It checks if the game is over; if not, it calculates the row and column of the click.
  // If the clicked cell is empty, it records the player's move, checks for a winner, and toggles the current player.
  
  // Handle mouse clicks on the game board
  If isGameOver Then
    Return True
  End If
  
  // Calculate the row and column based on the click position
  Var row As Integer = y \ CellHeight
  Var col As Integer = x \ CellWidth
  
  // If the clicked cell is empty, make a move
  If boardState(row, col) = 0 Then
    boardState(row, col) = currentPlayer
    StartAnimation(row, col)
    
    // Reset hover position after a move
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the affected cell
    Refresh(col * CellWidth, row * CellHeight, CellWidth, CellHeight)
    
    // Check for a winner or a draw
    Var winner As Integer = CheckWinner()
    If winner > 0 Then
      UpdateScore(winner)
      GameStatus("Player " + PlayerSymbol(winner) + " wins!", scoreX, scoreO)
      isGameOver = True
      GameOver("Player " + PlayerSymbol(winner) + " wins!", scoreX, scoreO)
      
      // Refresh the entire board to show the winning line
      Refresh(True)
    ElseIf Me.IsBoardFull() Then
      GameStatus("It's a draw!", scoreX, scoreO)
      isGameOver = True
      GameOver("Draw", scoreX, scoreO)
    Else
      // Switch to the other player
      currentPlayer = If(currentPlayer = 1, 2, 1)
      GameStatus("Player " + PlayerSymbol(currentPlayer) + "'s turn", PlayerSymbol(currentPlayer), scoreX, scoreO)
    End If
  End If
  
  Return True
End Function
  • Gestiona los movimientos del jugador.
  • Valida la legalidad del movimiento.
  • Comprueba las condiciones de ganador/empate.
  • Cambia el turno de jugadores.
Eventos MouseExit y MouseMove
Sub MouseExit() Handles MouseExit
  // This event is triggered when the mouse cursor exits the game board area.
  // It clears the hover effect to avoid leaving any visual artifacts on the board when the mouse is moved away.
  
  If hoverRow >= 0 And hoverRow < kBoardSize And hoverCol >= 0 And hoverCol < kBoardSize Then
    Var oldHoverRow As Integer = hoverRow
    Var oldHoverCol As Integer = hoverCol
    
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the cell that was previously hovered
    Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
  End If
End Sub
Sub MouseMove(x As Integer, y As Integer) Handles MouseMove
  // This event handles mouse movement over the game board.
  // It updates the hover effect when the mouse moves to a new cell, providing visual feedback.
  
  If Not isGameOver Then
    Var newHoverRow As Integer = y \ CellHeight
    Var newHoverCol As Integer = x \ CellWidth
    
    If newHoverRow <> hoverRow Or newHoverCol <> hoverCol Then
      Var oldHoverRow As Integer = hoverRow
      Var oldHoverCol As Integer = hoverCol
      
      hoverRow = newHoverRow
      hoverCol = newHoverCol
      
      // Refresh the old hover cell (if it was valid)
      If oldHoverRow >= 0 And oldHoverRow < 3 And oldHoverCol >= 0 And oldHoverCol < 3 Then
        Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
      End If
      
      // Refresh the new hover cell
      Refresh(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Proporciona retroalimentación visual
  • Registra el movimiento del apuntador sobre la rejilla
  • Refresca sólo las celdas que hayan cambiado
Evento Opening
Sub Opening() Handles Opening
  // This event initializes the board colors based on the current system theme (dark mode or light mode) and starts a new game.
  
  // Set up colors of the board lines, for the X's and O's
  If Color.IsDarkMode = True Then
    ColorBoard = Color.RGB(178, 161, 149)
    ColorX = Color.RGB(228, 182, 88)
    ColorO = Color.RGB(253, 161, 97)
  Else
    ColorBoard = Color.RGB(130, 110, 92)
    ColorX = Color.RGB(228, 182, 88)
    ColorO = Color.RGB(253, 161, 97)
  End If
  
  NewGame()
End Sub
  • Inicializa el tema de color.
  • Soporta los modos Oscuro y Claro.
  • Inicia automáticamente un nuevo juego.
Evento Paint
Sub Paint(g As Graphics, areas() As Rect) Handles Paint
  // This event is responsible for drawing the game board, hover effects, player symbols (X's and O's), and the winning line.
  // It is called whenever the game board needs to be redrawn.
  
  // Draw the board
  g.DrawingColor = ColorBoard
  g.DrawLine(Width/3, 0, Width/3, Height)
  g.DrawLine(2*Width/3, 0, 2*Width/3, Height)
  g.DrawLine(0, Height/3, Width, Height/3)
  g.DrawLine(0, 2*Height/3, Width, 2*Height/3)
  
  // Draw hover effect
  DrawHoverEffect(g)
  
  // Draw X's and O's
  g.PenSize = 8
  For row As Integer = 0 To 2
    For col As Integer = 0 To 2
      If boardState(row, col) = 1 Then
        DrawX(g, row, col)
      ElseIf boardState(row, col) = 2 Then
        DrawO(g, row, col)
      End If
    Next
  Next
  
  // Draw winning line if the game is over
  If isGameOver Then
    DrawWinningLine(g)
  End If
End Sub
  • Dibuja el tablero de juego.
  • Dibuja las líneas de la rejilla.
  • Gestiona el estado visual del juego.
  • Soporta dibujado dinámico.

Métodos de Animación y Dibujado

1. AnimationStep
Public Sub AnimationStep(sender As Timer)
  // This method is called by the animation timer (animationTimer property) to progress the animation of a newly placed symbol.
  // It increments the animation progress and stops the timer once the animation is complete.
  
  // Progress the animation
  animationProgress = animationProgress + 0.1
  If animationProgress >= 1 Then
    animationTimer.Enabled = False
    animationProgress = 1
  End If
  
  // Refresh only the cell being animated
  Refresh(lastPlayedCol * CellWidth, lastPlayedRow * CellHeight, CellWidth, CellHeight)
  
  // If the animation is complete, stop the timer
  If animationProgress >= 1 Then
    animationTimer.Enabled = False
  End If
End Sub
  • Gestiona la animación en la ubicación del símbolo.
  • Escala gradualmente el símbolo de 0 a 1.
  • Actualiza la celda sobre la que se ha jugado.
2. DrawX and DrawO
Sub MouseExit() Handles MouseExit
  // This event is triggered when the mouse cursor exits the game board area.
  // It clears the hover effect to avoid leaving any visual artifacts on the board when the mouse is moved away.
  
  If hoverRow >= 0 And hoverRow < kBoardSize And hoverCol >= 0 And hoverCol < kBoardSize Then
    Var oldHoverRow As Integer = hoverRow
    Var oldHoverCol As Integer = hoverCol
    
    hoverRow = -1
    hoverCol = -1
    
    // Refresh only the cell that was previously hovered
    Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
  End If
End Sub
Sub MouseMove(x As Integer, y As Integer) Handles MouseMove
  // This event handles mouse movement over the game board.
  // It updates the hover effect when the mouse moves to a new cell, providing visual feedback.
  
  If Not isGameOver Then
    Var newHoverRow As Integer = y \ CellHeight
    Var newHoverCol As Integer = x \ CellWidth
    
    If newHoverRow <> hoverRow Or newHoverCol <> hoverCol Then
      Var oldHoverRow As Integer = hoverRow
      Var oldHoverCol As Integer = hoverCol
      
      hoverRow = newHoverRow
      hoverCol = newHoverCol
      
      // Refresh the old hover cell (if it was valid)
      If oldHoverRow >= 0 And oldHoverRow < 3 And oldHoverCol >= 0 And oldHoverCol < 3 Then
        Refresh(oldHoverCol * CellWidth, oldHoverRow * CellHeight, CellWidth, CellHeight)
      End If
      
      // Refresh the new hover cell
      Refresh(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Dibuja los símbolos 0 y X con Animación
  • Centra el símbolo en la celda
  • Escala el símbolo basándose en el progreso de la animación
3. DrawHoverEffect
Public Sub DrawHoverEffect(g As Graphics)
  // This method draws a semi-transparent hover effect over the cell that the mouse is currently hovering over.
  // It only draws the effect if the game is not over and the hovered cell is empty.
  
  If Not isGameOver And hoverRow >= 0 And hoverRow < 3 And hoverCol >= 0 And hoverCol < 3 Then
    If boardState(hoverRow, hoverCol) = 0 Then
      g.DrawingColor = Color.RGB(255, 255, 255, 250) // Semi-transparent white
      g.FillRectangle(hoverCol * CellWidth, hoverRow * CellHeight, CellWidth, CellHeight)
    End If
  End If
End Sub
  • Proporciona retroalimentación visual sobre las celdas
  • Aplica una capa blanca semi-transparente
  • Sólo afecta a las celdas vacías, sobre las que no se ha jugado
4. DrawWinningLine
Public Sub DrawWinningLine(g As Graphics)
  // This method draws a semi-transparent green line over the winning combination on the board if a player has won.
  
  // Draw the winning line if there is a winner
  If winningLine.Count = 6 Then
    g.DrawingColor = Color.RGB(0, 255, 0, 128) // Semi-transparent green
    g.PenSize = 2
    
    Var startX As Integer = winningLine(1) * cellWidth + cellWidth / 2
    Var startY As Integer = winningLine(0) * cellHeight + cellHeight / 2
    Var endX As Integer = winningLine(5) * cellWidth + cellWidth / 2
    Var endY As Integer = winningLine(4) * cellHeight + cellHeight / 2
    
    g.DrawLine(startX, startY, endX, endY)
  End If
End Sub
  • Dibuja una línea semi-transparente de color verde
  • Resalta la combinación ganadora

Métodos de la Lógica del Juego

1. CheckWinner
Public Function CheckWinner() As Integer
  // This method checks the board for a winner by evaluating rows, columns, and diagonals.
  // It returns the winning player (1 or 2) or 0 if there is no winner.
  
  
  // Check rows
  For i As Integer = 0 To kBoardSize - 1
    If boardState(i, 0) <> 0 And boardState(i, 0) = boardState(i, 1) And boardState(i, 1) = boardState(i, 2) Then
      winningLine = Array(i, 0, i, 1, i, 2)
      Return boardState(i, 0)
    End If
  Next
  
  // Check columns
  For j As Integer = 0 To 2
    If boardState(0, j) <> 0 And boardState(0, j) = boardState(1, j) And boardState(1, j) = boardState(2, j) Then
      winningLine = Array(0, j, 1, j, 2, j)
      Return boardState(0, j)
    End If
  Next
  
  // Check diagonals
  If boardState(0, 0) <> 0 And boardState(0, 0) = boardState(1, 1) And boardState(1, 1) = boardState(2, 2) Then
    winningLine = Array(0, 0, 1, 1, 2, 2)
    Return boardState(0, 0)
  End If
  
  If boardState(0, 2) <> 0 And boardState(0, 2) = boardState(1, 1) And boardState(1, 1) = boardState(2, 0) Then
    winningLine = Array(0, 2, 1, 1, 2, 0)
    Return boardState(0, 2)
  End If
  
  winningLine.ResizeTo(-1)
  Return 0 // No winner yet
End Function
  • Explora el tablero en busca de combinaciones ganadoras.
  • Devuelve el jugador ganador o 0.
  • Almacena las coordenadas de la línea ganadora.
2. IsBoardFull
Public Function IsBoardFull() As Boolean
  // This method checks if the board is completely filled with no empty cells.
  // It returns true if the board is full, otherwise false.
  
  // Check if the board is full (no empty cells)
  For i As Integer = 0 To kBoardSize - 1
    For j As Integer = 0 To kBoardSize - 1
      If boardState(i, j) = 0 Then
        Return False
      End If
    Next
  Next
  Return True
End Function
  • Comprueba si todas las celdas están ocupadas.
  • Determina si el juego ha finalizado en empate.
3. NewGame
Public Sub NewGame()
  // This method resets the game state to start a new game.
  // It clears the board, resets the current player to Player 1, and updates the game status.
  
  // Reset the game state for a new game
  For i As Integer = 0 To 8
    boardState(i \ 3, i Mod 3) = 0
  Next
  currentPlayer = 1
  isGameOver = False
  animationProgress = 1
  If animationTimer <> Nil Then
    animationTimer.Enabled = False
  End If
  GameStatus("Player " + PlayerSymbol(currentPlayer) + "'s turn", scoreX, scoreO)
  winningLine.ResizeTo(-1)
  Refresh()
End Sub
  • Reinicia el juego a su estado inicial.
  • Limpia el tablero.
  • Reinicia los turnos de juego.
4. UpdateScore
Public Sub UpdateScore(winner As Integer)
  // This method updates the score for the winning player by incrementing the respective score counter.
  
  // Update the score for the winning player
  If winner = 1 Then
    scoreX = scoreX + 1
  ElseIf winner = 2 Then
    scoreO = scoreO + 1
  End If
End Sub
  • Incrementa el marcador para el jugador ganador.

Metodos Utilitarios

1. PlayerSymbol
Public Function PlayerSymbol(player As Integer) As String
  // This method returns the symbol ('X' or 'O') corresponding to the player number (1 or 2).
  Return If(player = 1, "X", "O")
End Function
  • Convierte el número de jugador a símbolo.
2. StartAnimation
Public Sub StartAnimation(row As Integer, col As Integer)
  // This method initializes and starts the animation for a newly placed symbol,
  // by setting the target cell and resetting the animation progress.
  
  // Start the animation for a newly placed symbol
  lastPlayedRow = row
  lastPlayedCol = col
  animationProgress = 0
  
  If animationTimer = Nil Then
    animationTimer = New Timer
    animationTimer.Period = 16 // equivalent of 60 FPS
    AddHandler animationTimer.Action, AddressOf AnimationStep
  End If
  
  animationTimer.Enabled = True
  animationTimer.RunMode = Timer.RunModes.Multiple
End Sub
  • Inicia la animación del símbolo.
  • Configura el temporizador para realizar la animación.

Instrucciones de Uso

Para utilizar el control TicTacToeGame en tu proyecto Xojo:

  • Arrastrar y Soltar: Arrastra el icono TicTacToeGame (debería tener el aspecto de un pequeño canvas) desde el Navegador (en el área izquierda del IDE) sobre Window1 o cualquier otra ventana sobre el que quieras usarlo.
    • Aparecerá una instancia de TicTacToeGame en la ventana. Puedes cambiar su tamaño y posición en caso necesario.
  • Inicialización: Si bien no es estrictamente requerido, es probable que quieras inicializar el juego en el manejador de evento Window1.Opening. Esto garantiza que el juego está listo para comenzar tan pronto como se abra la ventana. Puedes hacerlo añadiendo el siguiente código en el evento Window1.Opening:
    • // Assuming ‘TicTacToeGame1’ is the name of your control instance on the window TicTacToeGame1.NewGame
    • Esta línea llamará al método NewGame para tu control personalizado, configurando el tablero e iniciando un nuevo juego.
  • Ejecuta el Proyecto: Ejecuta el proyecto Xojo. Deberías de ver el tablero TicTacToe en tu ventana, listo para jugar.

Recomiendo descargar el proyecto completo de Xojo para el juego TicTacToe. Si encuentras algo que no esté lo suficientemente claro, puedes volver a este tutorial para encontrar explicaciones adicionales. Descarga el proyecto de Xojo desde este enlace.

Siguientes pasos:

¡Enhorabuena! Has creado con éxito la versión moderna y completamente funcional del juego de tres en raya con Xojo. Este proyecto supone una buena base a la hora de explorar el desarrollo de juegos y conceptos más avanzados, también sobre la animación y sobre cómo crear elementos de interfaz de usuario personalizados basados en el potente control DesktopCanvas de Xojo.

Ahora que has comprendido los fundamentos, te comento algunas ideas que puedes aplicar para llevar el juego de tres en raya al próximo nivel:

  • Inteligencia Artificial (IA): Implementa un oponente sencillo basado en IA, de modo que los jugadores puedan jugar contra el ordenador. Puedes empezar con algún generador de movimientos aleatorios y explorar a continuación algoritmos más sofisticados como Minimax. Xojo ya proporciona ejemplos sobre cómo integrar IA en proyectos.
  • Diferentes Modos de Juego: Añade opciones para tableros diferentes (por ejemplo 4×4, 5×5) o bien variaciones del juego de las tres en raya.
  • Multi-jugador en línea: Permite que los jugadores se reten entre sí utilizando las capacidades de conexión de red de Xojo.
  • UI/UX Mejoradas: Mejora la interfaz de usuario con gráficos personalizados, efectos de sonido y un aspecto más pulido. Considera añadir un temporizador.
  • Multiplataforma: Porta la clase TicTacToe a proyectos Mobile y Web.

Te animamos a experimentar, ser creativo y explorar estas capacidades. Comparte tus creaciones y conecta con otros desarrolladores de Xojo en los foros para aprender y crecer juntos.

¡Feliz programación!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *