Especialização em Sistemas com Python 3.12+ • Capítulo 17


🎯 Demonstração Prática: Aplicação Corporativa Desktop com Arquitetura MVC Completa

O exemplo abaixo implementa um painel cadastral desktop corporativo com arquitetura Model-View-Controller (MVC) estrita:

  • Model: Entidade AtivoCorporativo desacoplada com persistência em banco de dados SQLite.
  • View: Interface gráfica estruturada via gerenciador de geometria grid(), com campos tipados e tabela de listagem.
  • Controller: Orquestrador com validação defensiva de inputs, tratamento de erros e atualização reativa de tela.
  • Arquitetura segura que detecta automaticamente se o ambiente possui display gráfico disponível (os.environ.get("DISPLAY") ou Windows GUI), garantindo compatibilidade total com esteiras de CI/CD.
"""
demonstracao_mvc_desktop.py
Sistema Desktop de Cadastro de Ativos com Arquitetura MVC e Tkinter/CustomTkinter.
Compatível com Python 3.12+.
"""

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
import os
import sqlite3
import sys
import tkinter as tk
from tkinter import messagebox, ttk
from typing import Callable, Final

# ==============================================================================
# 1. CAMADA MODEL (DADOS E PERSISTÊNCIA PURA - ZERO DEPENDÊNCIA DE GUI)
# ==============================================================================
@dataclass(slots=True)
class AtivoCorporativo:
    id_ativo: int | None
    codigo_patrimonio: str
    descricao: str
    valor_aquisicao: Decimal

class AtivoRepository:
    def __init__(self, conexao: sqlite3.Connection) -> None:
        self._conn = conexao
        self._conn.row_factory = sqlite3.Row
        with self._conn:
            self._conn.execute("""
            CREATE TABLE IF NOT EXISTS ativos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                codigo TEXT UNIQUE NOT NULL,
                descricao TEXT NOT NULL,
                valor TEXT NOT NULL
            );
            """)

    def salvar(self, ativo: AtivoCorporativo) -> AtivoCorporativo:
        with self._conn:
            cursor = self._conn.execute(
                "INSERT INTO ativos (codigo, descricao, valor) VALUES (?, ?, ?);",
                (ativo.codigo_patrimonio, ativo.descricao, str(ativo.valor_aquisicao))
            )
            novo_id = cursor.lastrowid or 0
        return AtivoCorporativo(
            id_ativo=novo_id,
            codigo_patrimonio=ativo.codigo_patrimonio,
            descricao=ativo.descricao,
            valor_aquisicao=ativo.valor_aquisicao
        )

    def listar(self) -> list[AtivoCorporativo]:
        cursor = self._conn.execute("SELECT * FROM ativos ORDER BY id DESC;")
        return [
            AtivoCorporativo(
                id_ativo=r["id"],
                codigo_patrimonio=r["codigo"],
                descricao=r["descricao"],
                valor_aquisicao=Decimal(r["valor"])
            )
            for r in cursor.fetchall()
        ]

# ==============================================================================
# 2. CAMADA VIEW (APRESENTAÇÃO VISUAL EM GRID)
# ==============================================================================
class AtivoView(tk.Frame):
    """View contendo formulário em grid e listbox de ativos."""
    def __init__(self, master: tk.Tk, on_salvar: Callable[[str, str, str], None]) -> None:
        super().__init__(master, padx=15, pady=15)
        self.master: tk.Tk = master
        self.on_salvar_callback = on_salvar
        self.master.title("Controle Patrimonial de Ativos (MVC)")
        self.master.geometry("550x420")
        self.pack(fill=tk.BOTH, expand=True)

        self._construir_formulario()
        self._construir_tabela()

    def _construir_formulario(self) -> None:
        lbl_titulo = tk.Label(self, text="Cadastro de Ativos TI", font=("Arial", 14, "bold"))
        lbl_titulo.grid(row=0, column=0, columnspan=2, pady=(0, 15), sticky="w")

        tk.Label(self, text="Código Patrimônio:").grid(row=1, column=0, sticky="w", pady=4)
        self.ent_codigo = tk.Entry(self, width=30)
        self.ent_codigo.grid(row=1, column=1, sticky="ew", pady=4)

        tk.Label(self, text="Descrição do Item:").grid(row=2, column=0, sticky="w", pady=4)
        self.ent_descricao = tk.Entry(self, width=30)
        self.ent_descricao.grid(row=2, column=1, sticky="ew", pady=4)

        tk.Label(self, text="Valor Aquisição (R$):").grid(row=3, column=0, sticky="w", pady=4)
        self.ent_valor = tk.Entry(self, width=30)
        self.ent_valor.grid(row=3, column=1, sticky="ew", pady=4)

        self.btn_cadastrar = tk.Button(
            self, text="Salvar Ativo", command=self._disparar_salvar,
            bg="#0288d1", fg="white", font=("Arial", 10, "bold"), relief=tk.FLAT
        )
        self.btn_cadastrar.grid(row=4, column=0, columnspan=2, pady=12, sticky="ew")

    def _construir_tabela(self) -> None:
        self.tree = ttk.Treeview(self, columns=("codigo", "descricao", "valor"), show="headings", height=8)
        self.tree.heading("codigo", text="Código")
        self.tree.heading("descricao", text="Descrição")
        self.tree.heading("valor", text="Valor (R$)")

        self.tree.column("codigo", width=100)
        self.tree.column("descricao", width=250)
        self.tree.column("valor", width=100, anchor="e")

        self.tree.grid(row=5, column=0, columnspan=2, sticky="nsew", pady=5)
        self.grid_rowconfigure(5, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def _disparar_salvar(self) -> None:
        self.on_salvar_callback(
            self.ent_codigo.get(),
            self.ent_descricao.get(),
            self.ent_valor.get()
        )

    def limpar_campos(self) -> None:
        self.ent_codigo.delete(0, tk.END)
        self.ent_descricao.delete(0, tk.END)
        self.ent_valor.delete(0, tk.END)
        self.ent_codigo.focus_set()

    def atualizar_tabela(self, ativos: list[AtivoCorporativo]) -> None:
        for item in self.tree.get_children():
            self.tree.delete(item)
        for a in ativos:
            self.tree.insert("", tk.END, values=(a.codigo_patrimonio, a.descricao, f"R$ {a.valor_aquisicao:,.2f}"))

    def exibir_aviso(self, titulo: str, mensagem: str) -> None:
        messagebox.showwarning(titulo, mensagem)

    def exibir_sucesso(self, mensagem: str) -> None:
        messagebox.showinfo("Sucesso", mensagem)

# ==============================================================================
# 3. CAMADA CONTROLLER (LÓGICA DE ORQUESTRAÇÃO E VALIDAÇÃO)
# ==============================================================================
class AtivoController:
    def __init__(self, repository: AtivoRepository, root: tk.Tk) -> None:
        self.repository = repository
        self.view = AtivoView(root, on_salvar=self.salvar_novo_ativo)
        self.recarregar_dados()

    def salvar_novo_ativo(self, cod: str, desc: str, val_str: str) -> None:
        cod_limpo = cod.strip().upper()
        desc_limpa = desc.strip()

        if not cod_limpo or not desc_limpa:
            self.view.exibir_aviso("Validação", "Os campos Código e Descrição são obrigatórios.")
            return

        try:
            val_decimal = Decimal(val_str.replace(",", ".")).quantize(Decimal("0.01"))
            if val_decimal <= Decimal("0.00"):
                raise ValueError()
        except Exception:
            self.view.exibir_aviso("Validação", "O valor deve ser um número monetário positivo válido.")
            return

        novo_ativo = AtivoCorporativo(
            id_ativo=None,
            codigo_patrimonio=cod_limpo,
            descricao=desc_limpa,
            valor_aquisicao=val_decimal
        )

        try:
            self.repository.salvar(novo_ativo)
            self.view.exibir_sucesso(f"Ativo {novo_ativo.codigo_patrimonio} homologado com sucesso!")
            self.view.limpar_campos()
            self.recarregar_dados()
        except sqlite3.IntegrityError:
            self.view.exibir_aviso("Erro de Integridade", f"O código de patrimônio '{cod_limpo}' já está cadastrado.")

    def recarregar_dados(self) -> None:
        lista_atualizada = self.repository.listar()
        self.view.atualizar_tabela(lista_atualizada)

# ==============================================================================
# 4. INICIALIZAÇÃO
# ==============================================================================
if __name__ == "__main__":
    db_conn = sqlite3.connect(":memory:")
    repo = AtivoRepository(db_conn)

    # Popula dados de demonstração
    repo.salvar(AtivoCorporativo(None, "PAT-001", "MacBook Pro M3 Max 64GB", Decimal("28500.00")))
    repo.salvar(AtivoCorporativo(None, "PAT-002", "Monitor Dell Ultrasharp 32 4K", Decimal("5400.00")))

    # Execução condicional: se houver display gráfico, abre a janela; caso contrário, executa teste headless
    tem_display = sys.platform.startswith("win") or bool(os.environ.get("DISPLAY"))
    if tem_display:
        root_window = tk.Tk()
        app = AtivoController(repository=repo, root=root_window)
        # Para demonstração no terminal, encerra após 500ms se não interativo
        if "CI" in os.environ:
            root_window.after(500, root_window.destroy)
        root_window.mainloop()
    else:
        print("[HEADLESS] Ambiente sem display gráfico. Banco SQLite populado com sucesso:")
        for item in repo.listar():
            print(f"  * {item.codigo_patrimonio}: {item.descricao} (R$ {item.valor_aquisicao:,.2f})")