He estado trabajando en un generador de arrastrar y soltar para Python durante las últimas semanas.
Puedes comprobarlo en
Código fuente:
¿Qué puede hacer el constructor?
En resumen, puede ayudarlo a crear rápidamente una interfaz de usuario para Python y generar código de interfaz de usuario en múltiples bibliotecas/marcos, incluidos Tkinter y customtkinter. Puede leer más en
Pero no solo quiero lanzar un proyecto, también me gustaría compartir mi experiencia con ustedes. En este blog, repasaré mi proceso de pensamiento y una descripción general de alto nivel de cómo creé la aplicación.
Contrariamente a la creencia popular, Python se utiliza a menudo para crear aplicaciones rápidas, es especialmente popular entre los desarrolladores que trabajan en ciencia de datos, automatización, tareas de scripting, etc. Muchas herramientas internas y GUI, particularmente en entornos científicos y de investigación, se crean con Python debido a su simplicidad y la disponibilidad de marcos como Tkinter, PyQt y otros.
Ahora bien, había muchos constructores de arrastrar y soltar para la web, pero muy pocos para las GUI de Python, especialmente para tkinter. Vi algunos, pero el problema era que tenían una cantidad muy limitada de widgets o generaban código en formato XML, lo que no es ideal si estás desarrollando una IU en Python.
Entonces, al principio solo quería crear un generador de interfaz de usuario de arrastrar y soltar adecuado solo para Tkinter.
Seguí dándole vueltas a la idea de un generador de GUI ideal (sin juego de palabras). Me inspiré en la interfaz de usuario de Canva y se me ocurrieron algunas características que harían que mi GUI fuera ideal.
Entonces, a finales de julio, decidí comenzar a trabajar en el proyecto.
Al principio se llamó tkbuilder, lo que indica que es un generador de GUI para la biblioteca de interfaz de usuario de Tkinter.
Pero, si te diste cuenta, también puedo ampliar la misma idea para soportar múltiples bibliotecas y marcos de GUI de Python, ya que todo está hecho como un complemento y eso es exactamente lo que planeé hacer.
Para la versión inicial, no quería agregar demasiadas funciones que abrumaran a los usuarios. Quería crearla en función de los comentarios de las personas que la usaban. De esta manera, no pierdo el tiempo creando cosas que la gente no quiere.
Desde el principio decidí no tener un backend ni ningún otro formulario de registro. De esta manera, es mucho más sencillo para mí desarrollarlo y para los usuarios que lo usan. Solo quería un frontend simple con el que la gente pudiera comenzar a trabajar.
Sí, esto es algo en lo que estuve pensando durante bastante tiempo. La mayoría de los constructores de GUI para Python que existen se crearon con Python. Mi primera opción para Python fue PySide.
La aplicación basada en GUI más compleja que creé usando PyQt/Pyside fue una
Pero rápidamente me di cuenta de las limitaciones de usar Python para construir la versión inicial.
Typescript también era una opción, pero con Typescript siempre sentí que era demasiado verboso.
Éstas fueron las únicas cosas que noté inmediatamente, por lo que mi primera opción fue usar JS.
PD: Más tarde me arrepentí de no haber empezado con TS, pero esa será una historia para otro momento.
La biblioteca tipo framework con la que me siento más cómodo es React.js, pero crear una abstracción requeriría usar clases, lo que no se recomienda desde la introducción de los ganchos.
El problema de no usar un marco era que tendría que construir todo yo mismo y no tendría acceso a las vastas bibliotecas de componentes que React tiene para ofrecer.
Ambos tenían sus desventajas, pero las clases de React aún se pueden usar, por lo que para mí se convirtió en la opción obvia.
Comencé construyendo la base y la barra lateral a principios de agosto y tuve que parar por falta de fondos, así que me puse a trabajar en el proyecto de un cliente, que lamentablemente no me pagó el monto final. Probé con la financiación colectiva, pero tampoco tuve suerte.
Así que, en el mes de septiembre, con los pocos fondos que me quedaban, decidí lanzarme de lleno a este proyecto. Alrededor del 9 de septiembre, retomé el trabajo.
Se dedicó mucho tiempo a pensar en la abstracción base, que se puede ampliar para satisfacer las necesidades.
Quería tener un lienzo que se pudiera ampliar y mover de forma similar a Figma.
Un widget base desde el cual pueden extenderse todos los demás widgets.
Una función de arrastrar y soltar para arrastrar y soltar elementos de la interfaz de usuario en el lienzo.
Para construir con React, es necesario pensarlo y construirlo de una determinada manera. A pesar de la discusión sobre si es una biblioteca o un marco, siempre se siente más como un marco que como una biblioteca.
Siempre me gustó cómo Canva construyó su barra lateral, quería tener algo similar para mi generador de arrastrar y soltar.
Dibujé lo que tenía en mente en un papel. No soy el mejor artista del mundo 🙄
Entonces, ¿quién debería encargarse de arrastrar, redimensionar y seleccionar? ¿El lienzo o el widget base? ¿Cómo se manejarán los widgets dentro del widget?
¿El widget base conocerá a sus elementos secundarios o se gestionará con una única estructura de datos mediante el propio lienzo? ¿Cómo representaré los elementos secundarios dentro de los elementos secundarios?
¿Cómo funcionará la función de arrastrar y soltar dentro del lienzo y otros widgets?
¿Cómo se gestionarán los diseños?
Éstas fueron algunas de las preguntas que comencé a hacer antes de construir todo.
Aunque ahora la interfaz de usuario parece más sencilla, se pensó mucho en construir la base, para que parezca mucho más sencilla para los usuarios.
Enfoque basado en lienzo
Ahora HTML tiene un elemento Canvas predeterminado que permite hacer muchas cosas, como dibujar, agregar imágenes y demás. Ahora parecía un elemento ideal para usar en mi programa.
Entonces, comencé a verificar si existía una implementación de arrastrar y soltar, redimensionar, hacer zoom y desplazarse. Encontré
Intenté experimentar con Fabric.Js e intenté implementar todo en Fabric.js como puedes ver aquí.
Enfoque no basado en lienzo
Ahora, después de experimentar, el enfoque sin lienzo parecía mejor, ya que tengo acceso al administrador de diseño predeterminado provisto, además de que había muchos componentes de interfaz de usuario prediseñados disponibles que harían de esta una opción ideal al escalar.
Planeé simular un lienzo utilizando dos div diferentes: un div interno y un div contenedor externo.
Ahora, crear zoom y desplazarse era bastante fácil de implementar, ya que CSS ya tenía transformación, escala y traducción.
Primero, para implementar esto, tuve que tener un contenedor que contuviera un lienzo. Ahora, este lienzo es un elemento invisible (sin desbordamiento oculto), aquí es donde se colocan todos los elementos y se aplica el escalado y la traducción.
Para acercar tuve que incrementar la escala y para alejarla disminuírla.
Pruebe este sencillo ejemplo. (Tecla +
para hacer zoom y -
para alejar)
La panorámica funcionó de manera similar
Al empezar, había investigado sobre un par de bibliotecas como
Después de investigar, vi que React-beautiful-dnd ya no se mantenía y comencé con React dnd-kit. Cuando empecé a desarrollar, descubrí que la documentación de dnd-kit era bastante limitada para lo que estaba desarrollando. Además, pronto saldría una nueva versión con cambios importantes en la biblioteca, así que decidí dejar de usar React-dnd-kit hasta la versión principal.
Reescribí las partes en las que usé DND-kit con la API de arrastrar y soltar de HTML. La única limitación con la API de arrastrar y soltar nativa era que todavía no era compatible con algunos dispositivos táctiles, lo que no me importó porque estaba creando para dispositivos no táctiles.
Al crear una aplicación como esta, es fácil perder el control de todas las variables y los cambios, por lo que no puedo tener múltiples variables que controlen la misma información.
La información/estado de cada widget debe estar almacenada en el lienzo o en el propio widget, que luego pasa la información cuando se le solicita.
O tal vez usar una biblioteca de gestión de estado como Redux
Elegí tener toda la información sobre los widgets administrados por el componente Canvas después de experimentar diferentes enfoques.
La estructura de datos se parece a esto.
[ { id: "", // id of the widget widgetType: WidgetClass, // base widget children: [], // children will also have the same datastructure as the parent parent: "", // id of the parent of the current widget initialData: {} // information about the widget's data that's about to be rendered eg: backgroundColor, foregroundColor etc. } ]
Ahora quería cargar los recursos en la barra lateral a la que se podía acceder mediante la barra de herramientas de los widgets, pero cada vez que cambiaba de pestaña, la nueva renderización hacía que los recursos cargados desaparecieran.
Una de las mayores limitaciones de Redux es que solo se pueden almacenar datos serializables. Los datos no serializables, como imágenes, vídeos y otros recursos, no se pueden almacenar en Redux. Esto dificultaría la transferencia de datos comunes entre distintos componentes.
Una forma de solucionar este problema es usar React Context. En resumen, React Context ofrece una forma de pasar datos a través del árbol de componentes sin tener que pasar propiedades manualmente en cada nivel.
Todo lo que tendría que hacer para tener los datos en diferentes componentes sería envolverlos alrededor de un proveedor de contexto React.
Creé mis propios proveedores de contexto para dos cosas:
Aquí hay un ejemplo simple de cómo utilicé el contexto de React para arrastrar y soltar.
import React, { createContext, useContext, useState } from 'react' const DragWidgetContext = createContext() export const useDragWidgetContext = () => useContext(DragWidgetContext) // Provider component to wrap around parts that need drag-and-drop functionality export const DragWidgetProvider = ({ children }) => { const [draggedElement, setDraggedElement] = useState(null) const onDragStart = (element) => { setDraggedElement(element) } const onDragEnd = () => { setDraggedElement(null) } return ( <DragWidgetContext.Provider value={{ draggedElement, onDragStart, onDragEnd }}> {children} </DragWidgetContext.Provider> ) }
¡Sí! Eso es todo. Ahora solo me quedaba envolverlo alrededor del componente donde necesitaba el contexto, que en mi caso era sobre Canvas y la barra lateral.
Dado que cada widget se comporta de manera diferente y tiene sus propios atributos, decidí que los widgets deben ser responsables de generar su propio código y un motor de código solo manejará los conflictos de nombres de variables y de unir el código.
De esta manera, pude expandirme fácilmente para admitir muchos widgets prediseñados, así como algunos complementos de interfaz de usuario de terceros.
No tenía un backend ni un registro y había muchas empresas que ofrecían alojamiento gratuito para páginas estáticas. Primero decidí utilizar Vercel, pero muchas veces he visto que el alojamiento gratuito de Vercel deja de funcionar si había demasiadas solicitudes.
Fue entonces cuando me enteré de
Las únicas desventajas fueron que los tiempos de compilación eran bastante lentos y carecían de bastante documentación.
La parte más molesta del paso de compilación fue la falla de compilación. Funcionó en Vercel, pero no en las páginas de Cloudflare. Los registros tampoco eran tan claros. Y tenemos neumáticos gratuitos, solo tenemos 500 compilaciones por mes, así que no quería desperdiciar demasiadas.
Lo intenté durante horas y luego decidí configurar la integración continua en una cadena vacía.
CI='' npm install
Y finalmente se puso en marcha.
He estado construyendo todo esto en público. Si te interesa ver cómo ha evolucionado desde una simple barra lateral hasta un generador de arrastrar y soltar completamente desarrollado, puedes consultar el
#construirenpúblico
¡Oh! No te olvides de seguirnos para recibir actualizaciones.
Si te gustó este tipo de contenido, escribiré más blogs profundizando en cómo planifico y construyo cosas. Para seguirme, puedes suscribirte a mi boletín de substack :)