"""Elementos de GUI com PySide6 em torno de um canvas OpenGL. Aula 02 — Visualização 2D: primitivas gráficas. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python gui2d.py O que este exemplo mostra: * ``QMainWindow`` como janela principal, com **menu**, **barra de status** e um *widget* central; * um **menu** "Cor" que altera o estado da cena e pede o redesenho; * um **botão** "Fechar", em um layout ``QHBoxLayout``, acima do canvas; * a separação de papéis: o **canvas** (``QOpenGLWidget``) só sabe desenhar; a **janela** cuida da interface e envia comandos para o canvas. Regra de ouro: nunca chame funções ``gl*`` fora de ``initializeGL``, ``resizeGL`` ou ``paintGL``. Um *slot* de menu deve alterar um **atributo** do canvas e chamar ``update()``, que agenda um novo ``paintGL`` no momento certo, com o contexto OpenGL ativo. """ from __future__ import annotations import sys from PySide6.QtCore import Qt from PySide6.QtGui import QAction, QSurfaceFormat from PySide6.QtWidgets import ( QApplication, QHBoxLayout, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) from PySide6.QtOpenGLWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import gluOrtho2D class Canvas(QOpenGLWidget): """Canvas OpenGL que desenha um triângulo na cor corrente.""" def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) # Estado da cena: começa em vermelho. self.cor = (0.95, 0.30, 0.30) # Necessário para o canvas receber eventos de teclado. self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) def definir_cor(self, r: float, g: float, b: float) -> None: """Altera o estado da cena e agenda um redesenho do canvas.""" self.cor = (r, g, b) self.update() # NÃO chame gl* aqui: update() agenda o paintGL def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) def resizeGL(self, w: int, h: int) -> None: w, h = max(1, w), max(1, h) glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) glLoadIdentity() if w <= h: gluOrtho2D(-1.0, 1.0, -1.0 * h / w, 1.0 * h / w) else: gluOrtho2D(-1.0 * w / h, 1.0 * w / h, -1.0, 1.0) glMatrixMode(GL_MODELVIEW) glLoadIdentity() def paintGL(self) -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(*self.cor) glBegin(GL_TRIANGLES) glVertex2f(-0.5, -0.5) glVertex2f( 0.0, 0.5) glVertex2f( 0.5, -0.5) glEnd() # --- Eventos de entrada ------------------------------------------------- def mousePressEvent(self, event) -> None: # Coordenadas em pixels, com origem no canto SUPERIOR esquerdo # (convenção do Qt) — o OpenGL usa o canto INFERIOR esquerdo. pos = event.position().toPoint() janela = self.window() if isinstance(janela, QMainWindow): janela.statusBar().showMessage( f"Clique em ({pos.x()}, {pos.y()}) — " f"em coordenadas OpenGL, y = {self.height() - pos.y()}" ) def keyPressEvent(self, event) -> None: if event.key() == Qt.Key.Key_Escape: self.window().close() class JanelaPrincipal(QMainWindow): """Janela com menu, botão e o canvas OpenGL.""" def __init__(self) -> None: super().__init__() self.setWindowTitle("PySide6 + OpenGL — elementos de GUI") self.canvas = Canvas(self) botao_fechar = QPushButton("Fechar") botao_fechar.clicked.connect(self.close) # Layouts: uma linha de botões acima, o canvas ocupando o resto. linha_botoes = QHBoxLayout() linha_botoes.addWidget(botao_fechar) linha_botoes.addStretch() # empurra o botão para a esquerda coluna = QVBoxLayout() coluna.addLayout(linha_botoes) coluna.addWidget(self.canvas, stretch=1) central = QWidget() central.setLayout(coluna) self.setCentralWidget(central) self._montar_menu() self.statusBar().showMessage("Pronto — use o menu Cor ou clique no canvas") def _montar_menu(self) -> None: menu_cor = self.menuBar().addMenu("&Cor") for rotulo, atalho, rgb in ( ("&Vermelho", "R", (0.95, 0.30, 0.30)), ("V&erde", "G", (0.30, 0.95, 0.30)), ("&Azul", "B", (0.35, 0.55, 0.95)), ): acao = QAction(rotulo, self) acao.setShortcut(atalho) # lambda com valor default: "congela" o rgb desta iteração acao.triggered.connect(lambda _=False, c=rgb: self._trocar_cor(c)) menu_cor.addAction(acao) menu_cor.addSeparator() sair = QAction("&Sair", self) sair.setShortcut("Ctrl+Q") sair.triggered.connect(self.close) menu_cor.addAction(sair) def _trocar_cor(self, rgb: tuple[float, float, float]) -> None: self.canvas.definir_cor(*rgb) self.statusBar().showMessage(f"Cor do triângulo: RGB{rgb}") def main() -> int: fmt = QSurfaceFormat() fmt.setVersion(2, 1) fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) fmt.setDepthBufferSize(0) QSurfaceFormat.setDefaultFormat(fmt) app = QApplication(sys.argv) janela = JanelaPrincipal() janela.resize(640, 520) janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())