Video Tutorial

Documentación

En este tutorial aprenderás a instalar y ejecutar las dependencias necesarias para utilizar el script de Generación de Códigos QR en Python. Es un proceso sencillo compatible con Windows, macOS y Linux que te permitirá ejecutar la herramienta en tu computadora sin complicaciones.

¿Qué necesitas instalar?

  • Python 3.10 o superior — El lenguaje en el que está hecho el script.
  • customtkinter — Librería para la interfaz del programa.
  • qrcode[pil] y Pillow — Librerías encargadas de generar y procesar las imágenes de los códigos QR.

Instalación en Windows

Opción A: Instalador automático (más fácil)

Descarga el proyecto y haz doble clic en el archivo instalador.bat. Él instalará automáticamente todas las librerías necesarias.

Opción B: Instalación manual paso a paso

Paso 1 — Instalar Python

  1. Entra a python.org/downloads y descarga la versión más reciente.
  2. Abre el instalador y marca la casilla "Add Python to PATH" antes de continuar.
  3. Haz clic en Install Now y espera a que termine.

Paso 2 — Instalar las librerías

Abre el Símbolo del sistema (busca "cmd" en el menú inicio) y pega esto:

py -m pip install customtkinter qrcode[pil] Pillow

Paso 3 — Ejecutar el script

Haz doble clic en el archivo qr_generator.py o ejecútalo con:

py qr_generator.py

Instalación en macOS

Paso 1 — Instalar Python

Descarga desde python.org o usa Homebrew:

brew install python

Paso 2 — Instalar las librerías

pip3 install customtkinter "qrcode[pil]" Pillow

Paso 3 — Ejecutar el script

python3 qr_generator.py

Instalación en Linux (Ubuntu / Debian)

Paso 1 — Instalar Python y Tkinter

Verifica e instala Python con soporte gráfico:

sudo apt update && sudo apt install python3 python3-pip python3-tk -y

Paso 2 — Instalar las librerías

pip3 install customtkinter "qrcode[pil]" Pillow

Paso 3 — Ejecutar el script

python3 qr_generator.py

Librerías Necesarias

py -m pip install customtkinter qrcode[pil] Pillow

Código Fuente Principal

"""
Generador de Códigos QR — Interfaz Gráfica Moderna
Requiere: qrcode[pil], Pillow, customtkinter
"""

import customtkinter as ctk
from tkinter import filedialog, messagebox, colorchooser
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers.pil import (
    RoundedModuleDrawer, CircleModuleDrawer, SquareModuleDrawer,
    GappedSquareModuleDrawer, VerticalBarsDrawer, HorizontalBarsDrawer
)
from qrcode.image.styles.colormasks import SolidFillColorMask
from PIL import Image
import os
import threading

# ── Tema ────────────────────────────────────────────────────────────────────
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")

ACCENT      = "#6C63FF"
ACCENT2     = "#FF6584"
BG_DARK     = "#0F0F1A"
BG_CARD     = "#1A1A2E"
BG_WIDGET   = "#16213E"
TEXT_LIGHT  = "#E0E0FF"
TEXT_MUTED  = "#8888AA"

DRAWER_MAP = {
    "Cuadrado"      : SquareModuleDrawer,
    "Redondeado"    : RoundedModuleDrawer,
    "Circulos"      : CircleModuleDrawer,
    "Barras Vert."  : VerticalBarsDrawer,
    "Barras Horiz." : HorizontalBarsDrawer,
    "Separado"      : GappedSquareModuleDrawer,
}

ERROR_MAP = {
    "L - 7%"  : qrcode.constants.ERROR_CORRECT_L,
    "M - 15%" : qrcode.constants.ERROR_CORRECT_M,
    "Q - 25%" : qrcode.constants.ERROR_CORRECT_Q,
    "H - 30%" : qrcode.constants.ERROR_CORRECT_H,
}


def hex_to_rgb(hex_color: str) -> tuple:
    hex_color = hex_color.lstrip("#")
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))


class ColorButton(ctk.CTkFrame):
    """Boton compacto que muestra el color elegido y abre el selector."""

    def __init__(self, master, label: str, initial: str = "#000000", **kw):
        super().__init__(master, fg_color=BG_WIDGET, corner_radius=10, **kw)
        self.color = initial
        self._cb  = None

        ctk.CTkLabel(self, text=label, text_color=TEXT_MUTED,
                     font=("Segoe UI", 11)).pack(side="left", padx=(10, 6))

        self.swatch = ctk.CTkLabel(self, text="  ", width=28, height=22,
                                   corner_radius=6, fg_color=initial)
        self.swatch.pack(side="left")

        self.hex_lbl = ctk.CTkLabel(self, text=initial, text_color=TEXT_LIGHT,
                                    font=("Segoe UI Semibold", 11), width=70)
        self.hex_lbl.pack(side="left", padx=4)

        btn = ctk.CTkButton(self, text="✏", width=28, height=28,
                            fg_color=ACCENT, hover_color=ACCENT2,
                            font=("Segoe UI", 13), corner_radius=7,
                            command=self._pick)
        btn.pack(side="right", padx=8)

    def _pick(self):
        result = colorchooser.askcolor(color=self.color, title="Elegir color")
        if result and result[1]:
            self.set_color(result[1])
            if self._cb:
                self._cb(result[1])

    def set_color(self, hex_color: str):
        self.color = hex_color
        self.swatch.configure(fg_color=hex_color)
        self.hex_lbl.configure(text=hex_color)

    def on_change(self, callback):
        self._cb = callback


class QRApp(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("QR Studio — Generador de Codigos QR")
        self.geometry("980x720")
        self.minsize(900, 660)
        self.configure(fg_color=BG_DARK)

        self._qr_image = None
        self._preview_tk = None
        self._gen_thread = None

        self._build_ui()
        self.after(200, self._schedule_preview)  # genera un QR inicial

    # ── Construccion de la UI ────────────────────────────────────────────────

    def _build_ui(self):
        self.grid_columnconfigure(0, weight=2, minsize=380)
        self.grid_columnconfigure(1, weight=3)
        self.grid_rowconfigure(0, weight=1)

        self._build_left_panel()
        self._build_right_panel()

    def _build_left_panel(self):
        panel = ctk.CTkScrollableFrame(self, fg_color=BG_CARD, corner_radius=0,
                                       scrollbar_button_color=ACCENT)
        panel.grid(row=0, column=0, sticky="nsew")
        panel.grid_columnconfigure(0, weight=1)

        # ── Encabezado
        header = ctk.CTkFrame(panel, fg_color="transparent")
        header.grid(row=0, column=0, sticky="ew", padx=20, pady=(24, 10))

        ctk.CTkLabel(header, text="⬛  QR Studio",
                     font=("Segoe UI Black", 22), text_color=ACCENT).pack(anchor="w")
        ctk.CTkLabel(header, text="Crea codigos QR personalizados",
                     font=("Segoe UI", 12), text_color=TEXT_MUTED).pack(anchor="w")

        sep = ctk.CTkFrame(panel, height=2, fg_color=ACCENT, corner_radius=1)
        sep.grid(row=1, column=0, sticky="ew", padx=20, pady=(0, 16))

        row = 2

        # ── Contenido
        ctk.CTkLabel(panel, text="📝  Contenido del QR",
                     font=("Segoe UI Semibold", 13), text_color=TEXT_LIGHT,
                     anchor="w").grid(row=row, column=0, sticky="ew",
                                      padx=20, pady=(0, 6))
        row += 1

        self.content_text = ctk.CTkTextbox(panel, height=90, corner_radius=10,
                                           fg_color=BG_WIDGET, border_color=ACCENT,
                                           border_width=1, text_color=TEXT_LIGHT,
                                           font=("Segoe UI", 13))
        self.content_text.grid(row=row, column=0, sticky="ew", padx=20)
        self.content_text.insert("end", "https://usadosahora.com")
        self.content_text.bind("", lambda e: self._schedule_preview())
        row += 1

        # ── Plantillas rapidas
        quick = ctk.CTkFrame(panel, fg_color="transparent")
        quick.grid(row=row, column=0, sticky="ew", padx=20, pady=(8, 16))
        for i, (lbl, tmpl) in enumerate([
            ("🔗 URL", "https://"),
            ("📧 Email", "mailto:"),
            ("📞 Tel.", "tel:+"),
            ("📶 WiFi", "WIFI:T:WPA;S:MiRed;P:password;;"),
        ]):
            b = ctk.CTkButton(quick, text=lbl, width=80, height=28,
                              fg_color=BG_WIDGET, hover_color=ACCENT,
                              text_color=TEXT_LIGHT, font=("Segoe UI", 11),
                              corner_radius=7,
                              command=lambda t=tmpl: self._set_template(t))
            b.grid(row=0, column=i, padx=4)
        row += 1

        # ── Estilo de modulos
        self._section(panel, row, "🎨  Estilo de modulos")
        row += 1

        self.drawer_var = ctk.StringVar(value="Redondeado")
        seg = ctk.CTkSegmentedButton(panel, values=list(DRAWER_MAP.keys()),
                                     variable=self.drawer_var,
                                     font=("Segoe UI", 11),
                                     selected_color=ACCENT,
                                     selected_hover_color=ACCENT2,
                                     unselected_color=BG_WIDGET,
                                     command=lambda _: self._schedule_preview())
        seg.grid(row=row, column=0, sticky="ew", padx=20)
        row += 1

        # ── Correccion de errores
        self._section(panel, row, "🛡  Correccion de errores")
        row += 1
        self.error_var = ctk.StringVar(value="M - 15%")
        err_menu = ctk.CTkOptionMenu(panel, values=list(ERROR_MAP.keys()),
                                     variable=self.error_var,
                                     fg_color=BG_WIDGET, button_color=ACCENT,
                                     button_hover_color=ACCENT2,
                                     text_color=TEXT_LIGHT,
                                     font=("Segoe UI", 12), corner_radius=8,
                                     command=lambda _: self._schedule_preview())
        err_menu.grid(row=row, column=0, sticky="ew", padx=20)
        row += 1

        # ── Colores
        self._section(panel, row, "🎨  Colores")
        row += 1

        self.fg_btn = ColorButton(panel, "Modulos", "#000000")
        self.fg_btn.grid(row=row, column=0, sticky="ew", padx=20, pady=3)
        self.fg_btn.on_change(lambda _: self._schedule_preview())
        row += 1

        self.bg_btn = ColorButton(panel, "Fondo   ", "#FFFFFF")
        self.bg_btn.grid(row=row, column=0, sticky="ew", padx=20, pady=3)
        self.bg_btn.on_change(lambda _: self._schedule_preview())
        row += 1

        # ── Tamano de caja
        self._section(panel, row, "📐  Tamano de caja (px)")
        row += 1

        self.box_size_var = ctk.IntVar(value=10)
        ctk.CTkSlider(panel, from_=4, to=30,
                      variable=self.box_size_var, number_of_steps=26,
                      button_color=ACCENT, button_hover_color=ACCENT2,
                      progress_color=ACCENT,
                      command=lambda _: self._schedule_preview()
                      ).grid(row=row, column=0, sticky="ew", padx=20)
        row += 1

        self.box_lbl = ctk.CTkLabel(panel, text="10 px", font=("Segoe UI", 11),
                                    text_color=TEXT_MUTED, anchor="e")
        self.box_lbl.grid(row=row, column=0, sticky="ew", padx=24, pady=(0, 4))
        self.box_size_var.trace_add("write",
            lambda *_: self.box_lbl.configure(text=f"{self.box_size_var.get()} px"))
        row += 1

        # ── Borde (quiet zone)
        self._section(panel, row, "⬜  Borde (quiet zone)")
        row += 1

        self.border_var = ctk.IntVar(value=4)
        ctk.CTkSlider(panel, from_=0, to=10,
                      variable=self.border_var, number_of_steps=10,
                      button_color=ACCENT, button_hover_color=ACCENT2,
                      progress_color=ACCENT,
                      command=lambda _: self._schedule_preview()
                      ).grid(row=row, column=0, sticky="ew", padx=20)
        row += 1

        self.border_lbl = ctk.CTkLabel(panel, text="4", font=("Segoe UI", 11),
                                       text_color=TEXT_MUTED, anchor="e")
        self.border_lbl.grid(row=row, column=0, sticky="ew", padx=24, pady=(0, 4))
        self.border_var.trace_add("write",
            lambda *_: self.border_lbl.configure(text=str(self.border_var.get())))
        row += 1

        # ── Botones de accion
        actions = ctk.CTkFrame(panel, fg_color="transparent")
        actions.grid(row=row, column=0, sticky="ew", padx=20, pady=(16, 24))
        actions.grid_columnconfigure((0, 1), weight=1)

        ctk.CTkButton(actions, text="💾  Guardar PNG",
                      height=44, corner_radius=12,
                      fg_color=ACCENT, hover_color=ACCENT2,
                      font=("Segoe UI Semibold", 14),
                      command=self._save_png
                      ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ctk.CTkButton(actions, text="📄  Guardar SVG",
                      height=44, corner_radius=12,
                      fg_color=BG_WIDGET, hover_color=ACCENT,
                      font=("Segoe UI Semibold", 14),
                      border_color=ACCENT, border_width=1,
                      command=self._save_svg
                      ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

    def _build_right_panel(self):
        panel = ctk.CTkFrame(self, fg_color=BG_DARK, corner_radius=0)
        panel.grid(row=0, column=1, sticky="nsew")
        panel.grid_rowconfigure(1, weight=1)
        panel.grid_columnconfigure(0, weight=1)

        # Status bar
        self.status_var = ctk.StringVar(value="Listo")
        status_bar = ctk.CTkFrame(panel, fg_color=BG_CARD, height=36,
                                  corner_radius=0)
        status_bar.grid(row=0, column=0, sticky="ew")
        ctk.CTkLabel(status_bar, textvariable=self.status_var,
                     font=("Segoe UI", 12), text_color=TEXT_MUTED,
                     anchor="w").pack(side="left", padx=16)

        # Preview area
        preview_frame = ctk.CTkFrame(panel, fg_color=BG_DARK, corner_radius=0)
        preview_frame.grid(row=1, column=0, sticky="nsew", padx=40, pady=40)
        preview_frame.grid_rowconfigure(0, weight=1)
        preview_frame.grid_columnconfigure(0, weight=1)

        self.preview_label = ctk.CTkLabel(preview_frame, text="⏳ Generando...",
                                          fg_color=BG_CARD, corner_radius=20,
                                          text_color=TEXT_MUTED,
                                          font=("Segoe UI", 16))
        self.preview_label.grid(row=0, column=0, sticky="nsew")

        # Boton de copiar
        ctk.CTkButton(panel, text="📋  Copiar al portapapeles",
                      height=38, corner_radius=10,
                      fg_color=BG_CARD, hover_color=BG_WIDGET,
                      font=("Segoe UI", 12), text_color=TEXT_MUTED,
                      command=self._copy_to_clipboard
                      ).grid(row=2, column=0, sticky="ew", padx=40, pady=(0, 20))

    # ── Helpers ──────────────────────────────────────────────────────────────

    def _section(self, parent, row: int, text: str):
        ctk.CTkLabel(parent, text=text, font=("Segoe UI Semibold", 13),
                     text_color=TEXT_LIGHT, anchor="w").grid(
            row=row, column=0, sticky="ew", padx=20, pady=(14, 4))

    def _set_template(self, tmpl: str):
        self.content_text.delete("1.0", "end")
        self.content_text.insert("end", tmpl)
        self._schedule_preview()

    def _get_content(self) -> str:
        return self.content_text.get("1.0", "end").strip()

    # ── Generacion del QR ─────────────────────────────────────────────────────

    def _schedule_preview(self, _=None):
        """Lanza la generacion en un hilo para no bloquear la UI."""
        if self._gen_thread and self._gen_thread.is_alive():
            return
        self._gen_thread = threading.Thread(target=self._generate, daemon=True)
        self._gen_thread.start()

    def _generate(self):
        self.after(0, lambda: self.status_var.set("⏳  Generando..."))
        try:
            content  = self._get_content()
            if not content:
                self.after(0, lambda: self.status_var.set("⚠  Sin contenido"))
                return

            drawer   = DRAWER_MAP[self.drawer_var.get()]()
            ec_level = ERROR_MAP[self.error_var.get()]
            box_size = self.box_size_var.get()
            border   = self.border_var.get()
            fg_rgb   = hex_to_rgb(self.fg_btn.color)
            bg_rgb   = hex_to_rgb(self.bg_btn.color)

            qr = qrcode.QRCode(
                error_correction=ec_level,
                box_size=box_size,
                border=border,
            )
            qr.add_data(content)
            qr.make(fit=True)

            img = qr.make_image(
                image_factory=StyledPilImage,
                module_drawer=drawer,
                color_mask=SolidFillColorMask(
                    front_color=fg_rgb,
                    back_color=bg_rgb
                )
            )
            self._qr_image = img.convert("RGBA")
            version = qr.version
            modules = version * 4 + 17
            self.after(0, self._update_preview)
            self.after(0, lambda: self.status_var.set(
                f"✅  QR listo · version {version} · {modules}x{modules} modulos"))
        except Exception as e:
            err = str(e)
            self.after(0, lambda: self.status_var.set(f"❌  Error: {err}"))

    def _update_preview(self):
        if self._qr_image is None:
            return
        pw = self.preview_label.winfo_width()  or 400
        ph = self.preview_label.winfo_height() or 400
        size = min(pw, ph) - 40
        if size < 80:
            size = 360
        img_resized = self._qr_image.copy()
        img_resized.thumbnail((size, size), Image.LANCZOS)

        ctk_img = ctk.CTkImage(light_image=img_resized,
                               dark_image=img_resized,
                               size=(size, size))
        self._preview_tk = ctk_img
        self.preview_label.configure(image=self._preview_tk, text="")

    # ── Guardado ─────────────────────────────────────────────────────────────

    def _save_png(self):
        if self._qr_image is None:
            messagebox.showwarning("Aviso", "Primero genera un QR.")
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".png",
            filetypes=[("PNG image", "*.png"), ("JPEG image", "*.jpg"),
                       ("BMP image", "*.bmp")],
            title="Guardar codigo QR",
            initialfile="mi_qr.png"
        )
        if path:
            img = self._qr_image
            if path.lower().endswith((".jpg", ".jpeg")):
                img = img.convert("RGB")
            img.save(path)
            self.status_var.set(f"💾  Guardado en {os.path.basename(path)}")
            messagebox.showinfo("Guardado", f"QR guardado en:\n{path}")

    def _save_svg(self):
        """Genera y guarda un SVG usando qrcode."""
        content = self._get_content()
        if not content:
            messagebox.showwarning("Aviso", "Sin contenido para generar el QR.")
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".svg",
            filetypes=[("SVG image", "*.svg")],
            title="Guardar QR como SVG",
            initialfile="mi_qr.svg"
        )
        if not path:
            return
        try:
            import qrcode.image.svg as qr_svg
            factory = qr_svg.SvgPathImage
            ec_level = ERROR_MAP[self.error_var.get()]
            img = qrcode.make(
                content,
                image_factory=factory,
                error_correction=ec_level,
            )
            img.save(path)
            self.status_var.set(f"📄  SVG guardado en {os.path.basename(path)}")
            messagebox.showinfo("Guardado", f"SVG guardado en:\n{path}")
        except Exception as e:
            messagebox.showerror("Error", str(e))

    def _copy_to_clipboard(self):
        """Copia la imagen QR al portapapeles (Windows)."""
        if self._qr_image is None:
            messagebox.showwarning("Aviso", "Primero genera un QR.")
            return
        try:
            import win32clipboard
            from io import BytesIO
            output = BytesIO()
            img = self._qr_image.convert("RGB")
            img.save(output, "BMP")
            data = output.getvalue()[14:]
            output.close()
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
            win32clipboard.CloseClipboard()
            self.status_var.set("📋  Imagen copiada al portapapeles!")
        except ImportError:
            tmp = os.path.join(os.environ.get("TEMP", "."), "_qr_temp.png")
            self._qr_image.convert("RGB").save(tmp)
            self.status_var.set("⚠  pywin32 no instalado. QR guardado en TEMP")
            messagebox.showinfo(
                "Portapapeles",
                f"pywin32 no esta instalado.\nQR guardado temporalmente en:\n{tmp}"
            )


if __name__ == "__main__":
    app = QRApp()
    app.mainloop()