sexta-feira, 15 de janeiro de 2010

FOX Toolkit + OpenCV - Vídeo

Recentemente, uma amiga minha me pediu um exemplo de como exibir um vídeo carregado pelo OpenCV utilizando o FOX.

Então eu rapidamente fiz um programinha para ajudá-la: um micro-player de vídeo. Micro porque não faz nada além de exibir os quadros. Sem som (OpenCV não trabalha com som). Sem controles, nem nada. Abriu o vídeo e pronto.

Obs.: Este tópico, assim como os outros relacionados ao OpenCV, são direcionados àqueles que já tem experiência com o FOX Toolkit, especialmente este aqui por conter itens que ainda não tratei. Tratarei todos esses assuntos em tópicos futuros.

Bom, vamos lá.

FoxVideoPlayerMainWindow.h

Declaração dos objetos


FXImage *image;
Esta é a imagem que será desenhada no canvas (área de desenho), representando cada quadro do vídeo.

FXCanvas *canvas;
Esta é a área de desenho propriamente dita.

CvCapture *capture;
Uma estrutura sinistra que o OpenCV utiliza para guardar as informações do vídeo sendo capturado. Sinistra porque a documentação não revela a estrutura interna. Para conhecê-la, é preciso ir no fonte do OpenCV, coisa que eu ainda não quis fazer.

IplImage *frame;
Cada quadro do vídeo é uma IplImage. Precisa dizer mais?

double freq;
Frequência de atualização. É um vídeo, não é? Não pode ficar no mesmo quadro para sempre.

IplImage *dummy;
Essa é uma pseudo-imagem apenas para converter os quadros para a representação interna do FOX, conforme discutido aqui.

Eventos

Existem apenas três eventos: pintar o canvas, carregar um vídeo e pegar o próximo quadro. Assim:

public:
enum {
ID_CANVAS = FXMainWindow::ID_LAST,
ID_LOAD_VIDEO,
ID_FRAME,
};
/* Pintar o canvas */
long onPaintCanvas(FXObject*, FXSelector, void*);
/* Carregar o vídeo */
long onCmdLoadVideo(FXObject*, FXSelector, void*);
/* Pegar cada quadro do vídeo */
long onTimeoutFrame(FXObject*, FXSelector, void*);
E é isso. Agora, vamos implementar essa cambada.


FoxVideoPlayerMainWindow.cpp

Construtor da janela

FoxVideoPlayerMainWindow::FoxVideoPlayerMainWindow(FXApp *a)
: FXMainWindow(a, "FoxVideoPlayer", 0, 0, DECOR_ALL) {
FXToolBar *toolBar = new FXToolBar(this, this);
new FXButton(toolBar, "Abrir vídeo", 0, this, ID_LOAD_VIDEO,
BUTTON_NORMAL|BUTTON_TOOLBAR);
canvas = new FXCanvas(this, this, ID_CANVAS,
LAYOUT_FIX_WIDTH|LAYOUT_FIX_HEIGHT, 0, 0, 0, 0);
image = 0;
capture = 0;
frame = 0;
dummy = 0;
}
Aqui eu simplesmente crio uma barra de ferramentas com um botão para carregar um vídeo, crio o canvas e inicializo os demais objetos com nulo. Nada de mais.

Pintando o canvas

long FoxVideoPlayerMainWindow::onPaintCanvas(FXObject*, FXSelector, void *ptr) {
FXEvent *ev = static_cast<FXEvent*>(ptr);
FXDCWindow dc(canvas, ev);
if (frame) {
dc.drawImage(image, 0, 0);
}
else {
dc.setForeground(FXRGB(0, 0, 0));
dc.fillRectangle(0, 0, canvas->getWidth(), canvas->getHeight());
}
return 1;
}
Duas opções: se houver um quadro a desenhar, desenha. Senão, desenha um retângulo preto. Simples.

Carregando o vídeo

Essa função é um pouco grande, então vou quebrá-la em algumas partes.

long FoxVideoPlayerMainWindow::onCmdLoadVideo(FXObject*, FXSelector, void*) {
FXFileDialog dialog(this, "Abrir vídeo");
dialog.setDirectory(FXSystem::getHomeDirectory());
if (!dialog.execute(PLACEMENT_OWNER))
return 1;
FXString filename = dialog.getFilename();
Exibe uma caixa de diálogo para selecionar o vídeo.

capture = cvCaptureFromFile(filename.text());
if (!capture) {
FXMessageBox::error(this, MBOX_OK, "Erro", "Erro ao abrir o vídeo");
return 1;
}
Abre o vídeo e armazena na estrutura obscura. Um erro e a função cvCaptureFromFile() retorna nulo.

int width = (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH);
int height = (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT);
freq = cvGetCaptureProperty(capture, CV_CAP_PROP_FPS);
freq = 1.0 / freq * 1000.0;
Eu preciso saber as dimensões do vídeo e o número de quadros por segundo. A função cvGetCaptureProperty() fornece diversas informações sobre o vídeo. As informações disponíveis são descritas na documentação do OpenCV.

Entretanto, é preciso fazer esse cálculo para saber, digamos assim, o tempo de permanência de cada quadro em milissegundos (o porquê mais para frente).

canvas->resize(width, height);
resize(getDefaultWidth(), getDefaultHeight());
Agora que eu sei o tamanho do vídeo, redimensiono o canvas. Depois dele a janela principal.

Explicando rapidamente o que acontece aqui: por padrão, a janela principal é configurada para ficar com o menor tamanho em que caibam todos os filhos. Como eu redimensionei um deles (depois de todos criados), as funções getDefaultWidth() e getDefaultHeight() retornam as dimensões necessárias para caber todo mundo novamente.

image = new FXImage(getApp(), 0, IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
width, height);
image->create();
dummy = cvCreateImageHeader(cvSize(width, height), IPL_DEPTH_8U, 4);
cvSetData(dummy, image->getData(), CV_AUTO_STEP);
Criando as imagens. Nada de especial, já falei sobre isso.

getApp()->addTimeout(this, ID_FRAME, 0);
return 1;
}
Bom, a última coisa a fazer aqui é disparar um timer para recuperar os quadros do vídeo e desenhá-los. Não queremos esperar, então o tempo é 0.

Quadro-a-quadro

long FoxVideoPlayerMainWindow::onTimeoutFrame(FXObject*, FXSelector, void*) {
if (!capture)
return 1;
frame = cvQueryFrame(capture);
if (frame) {
cvCvtColor(frame, dummy, CV_BGR2RGBA);
image->render();
canvas->update();
getApp()->addTimeout(this, ID_FRAME, freq);
}
else
canvas->update();
return 1;
}
Toda vez que essa função é chamada, o próximo quadro do vídeo é recuperado com cvQueryFrame(). Se não for nulo (ou seja, o vídeo ainda não acabou), os espaços de cor são convertidos com cvCvtColor(). Como dummy utiliza os pixels de image, é como se eu estivesse pintando duas imagens de uma vez só (mas, na verdade, é uma imagem só com duas representações).

Em seguida, vem uma operação importante. É algo que eu demorei muito para entender. Uma imagem (pelo menos no FOX Toolkit, não sei quando a outras bibliotecas de interface gráfica) tem duas representações, uma no lado do cliente outra do servidor. O lado do cliente é esse vetor de pixels que pode ser acessado por meio de image->getData(). O lado do servidor é o servidor gráfico (no meu caso, o X; não sei qual seria no caso do Uíndous ou do Macos). Quando o lado do cliente é alterado, é necessário transferir os pixels para o servidor com render(). Sem isso, seria exibida sempre a mesma imagem que foi criada, apenas um retângulo preto. Só por curiosidade, restore() faz o inverso, recupera os pixels do servidor e põe de volta no cliente.

Tendo a nova imagem no lado do servidor, basta atualizar o canvas. Depois, agenda a recuperação do próximo quadro, desta vez respeitando a frequência de quadros do vídeo.

Quando o vídeo termina, cvQueryFrame() returna nulo. Neste caso, não é mais necessário agendar o próximo quadro. Apenas atualiza o canvas, que será pintado de preto, apenas para não ficar exibindo o último quadro do vídeo.


Resultado

O resultado disso tudo pode ser observado neste vídeo que eu gravei:




Conclusão

Bem, isto foi apenas o básico necessário. Apenas exibi o vídeo, sem aplicar nenhum processamento. Isso, obviamente, deve ser feito depois de recuperar o quadro e antes de convertê-lo para RGBA.

Além disso, no exemplo o vídeo é carregado de um arquivo, mas o OpenCV também captura vídeo pela webcam.

Um abraço e até a próxima.

segunda-feira, 28 de dezembro de 2009

IplImage e FXImage: revisitado

Anteriormente, falei aqui como utilizar uma imagem do OpenCV (IplImage) e uma do FOX (FXImage), ambas compartilhando o vetor de pixels. Confira aqui.

Pois bem. Fazendo daquela maneira, há certos problemas: ambos objetos devem permanecer vivos, e o dono dos pixels tem que ser a IplImage.

Isto pode não ser desejado. Por exemplo, vou citar o caso que aconteceu comigo e tive que matar um pouco a cabeça para resolver.

Muitas imagens que eu exibo (com o FOX Toolkit) precisam antes passar por algum processamento (com o OpenCV). Depois disso, eu não preciso mais da IplImage, apenas dos pixels.

Entretanto, seguir os passos explicados no outro tópico levava a um problema seriíssimo, que será explicado no decorrer deste tópico.

Compartilhando pixels: antigamente

Conforme eu havia explicado, criava-se primeiro a IplImage e depois a FXImage, utilizando o vetor de pixels, mais ou menos assim:


IplImage *iplImage = cvCreateImage(cvSize(200, 200), IPL_DEPTH_8U, 4);

FXImage *fxImage = new FXImage(getApp(), iplImage->imageData,
    IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
    iplImage->width, iplImage->height);


Lembrando que era necessário um cast horroroso com reinterpret_cast para transformar os pixels da IplImage de char* para FXColor.

Com isso, eu tenho que minha IplImage "doou" seus pixels para a FXImage, que tomou conta deles. Assim, a IplImage poderia ser dispensada (na verdade, somente o cabeçalho da imagem; o OpenCV permite isso) que a FXImage, quando for liberada, libera também o vetor de pixels.

O problema

Isso causa um problema que eu demorei muito para descobrir. E, depois de descoberto, vê-se que é uma coisa tão simples... é sempre um detalhezinho que cega a gente.

O grande problema é que, como o vetor de pixels é alocado pela IplImage, ele é alocado usando malloc. A FXImage, quando tenta liberar, tenta com delete. Aí não dá certo...

A "solução"

Eu criava as imagens independentemente, cada uma com seu vetor de pixels. Depois de processada, eu fazia uma chamada a memcpy para copiar os pixels. Uma tristeza considerando o contexto.

Compartilhando pixels: a solução de fato

Depois de quase desistir da idéia, eu descobri (dia desses...) o problema. E logo cheguei a uma solução elegante (e óbvia).

Basta criar primeiro a FXImage, que vai apenas "emprestar" os pixels para a IplImage. Assim:


FXImage *fxImage = new FXImage(getApp(), 0,
    IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
    200, 200);


Percebam que o segundo parâmetro, que seria o vetor de pixels, é nulo. Como os pixels são de responsabilidade da FXImage, ela mesma os cria (isso é informado pela opção IMAGE_OWNED).

É necessário também manter os pixels no lado do cliente (opção IMAGE_KEEP). Sem ela, os pixels são liberados depois da imagem criada, impossibilitando o compartilhamento.

Pois bem, agora vamos "emprestar" os pixels da FXImage para a IplImage:


IplImage *iplImage = cvCreateImageHeader(cvSize(200, 200),
    IPL_DEPTH_8U, 4);
  cvSetData(iplImage, fxImage->getData(), CV_AUTOSTEP);


cvCreateImageHeader() aloca todas as informações de uma imagem, com exceção do vetor de pixels. Com isso, a IplImage, que é temporária, apenas pega emprestado os pixels para trabalhar sobre eles. Ao terminar o processamento, podemos liberar o cabeçalho:


cvReleaseImageHeader(&iplImage);


Uma grande vantagem dessa abordagem é que não precisa nem converter a imagem, conforme eu havia feito anteriormente. A imagem já é criada no formato da FXImage.

Conclusão

Conforme havia dito, é uma coisa tão simples e óbvia que fica invisível.

Espero que essa informação seja útil para quem for usar essas duas bibliotecas juntas.

Abraço e até a próxima.

quarta-feira, 23 de dezembro de 2009

Submenus

Dando continuidade à nossa série sobre Menus, falarei agora sobre como adicionar submenus. Vamos lá.

Submenus são menus como quaisquer outros

Isso mesmo. O processo de criar um submenu é praticamente o mesmo de criar um menu "normal".

Para exemplificar o processo, vamos criar um menu "Exibir", com um submenu "Zoom". Neste caso, temos dois painéis, que serão declarados da seguinte maneira:

FXMenuPane *_viewMenu;
FXMenuPane *_zoomMenu;

Lembrando que eles devem estar no escopo de classe, pois precisam ser liberados manualmente.

Percebam que o submenu "Zoom" nada mais é que um FXMenuPanel como todos os outros que já vimos até aqui. Agora vamos criá-los, novamente recuando o código para indicar a hierarquia.

_viewMenu = new FXMenuPane(this);
  _zoomMenu = new FXMenuPane(this);
    new FXMenuCommand(_zoomMenu, "&Aumentar\tCtrl++");
    new FXMenuCommand(_zoomMenu, "&Normal\tCtrl+0");
    new FXMenuCommand(_zoomMenu, "&Diminuir\tCtrl+-");

    new FXMenuSeparator(_zoomMenu);

    new FXMenuCommand(_zoomMenu, "A&justar\tHome");
  new FXMenuCascade(_viewMenu, "&Zoom", 0, _zoomMenu);
new FXMenuTitle(menuBar, "&Exibir", 0, _viewMenu);

Explicando...

Na primeira linha, aloco o menu "Exibir".

Na segunda, aloco o (sub)menu "Zoom". Chamando atenção para o recuo, indicando que _zoomMenu será posicionado "dentro" de _viewMenu;

Nas linhas seguintes, preencho com alguns comandos e um separador. Nada além do trivial com recuo.

Em seguida, criamos a entrada no menu "Exibir" que vai expandir o menu "Zoom". Repetindo:

new FXMenuCascade(_viewMenu, "&Zoom", 0, _zoomMenu);

Os parâmetros são os mesmos de FXMenuTitle (comparem com a última linha):
  • O FXComposite onde ficará localizado o submenu. Neste caso, adicionamos ao painel "Exibir"
  • O título que vai aparecer no painel, com direito a mnemônico
  • Ícone
  • O painel que será exibido ao mover o mouse sobre o título.

Liberando

Como todo bom painel de menu, deve ser liberado manualmente:


delete _viewMenu;
delete _zoomMenu;

Só uma observação: já tratei esse assunto aqui, então é só um lembrete. Quando eu falo em deletar manualmente, é porque estou usando o FOX 1.6. Na versão atual de desenvolvimento (1.7), existe uma classe chamada FXAutoPtr, que é um "ponteiro inteligente". Resumindo a história, ele guarda um ponteiro e automaticamente o libera quando sai de escopo. Veja a descrição aqui (na verdade, uma dúvida enviada à lista de discussão do FOX que eu traduzi e postei aqui).

Resultado

Eis o resultado do nosso submenu:


segunda-feira, 14 de dezembro de 2009

Menus

Neste tópico, ensinarei como adicionar uma barra de menus (com menus, obviamente) ao programa. Neste momento, colocarei menus simples, apenas com comandos simples. Há outros componentes que podem ser adicionados aos menus, mas serão tratados em tópicos futuros.

Vamos lá.


Barra de menus

Tudo começa com a barra de menus. Naturalmente, é lá que ficam os menus. E, naturalmente, fica no topo da janela:



FXMenuBar *menuBar = new FXMenuBar(this, LAYOUT_FILL_X|FRAME_RAISED);


Percebam que ela pode ser criada localmente, o próprio FOX se encarrega de liberá-la depois. Além disso, lembrem-se que a localização padrão é no topo da janela, portanto não precisei passar LAYOUT_SIDE_TOP.

Isso cria o espaço ideal para os menus. Aqui, criarei apenas dois, "Arquivo" e "Editar", apenas para exemplificar. Não é um processo complicado. Pelo contrário, é bem simples.


Adicionando menus

Os menus precisam ser declarados com escopo de classe. Então, na declaração da classe, temos:



protected:
  FXMenuPane *_fileMenu;
  FXMenuPane *_editMenu;


Agora, vamos criar e preencher os menus. Neste primeiro exemplo, do menu "Arquivo", vou colocar e explicar linha por linha. No segundo, o menu "Editar", vou colocar tudo de uma vez só para explicar a forma como eu gosto de criar os menus.

Menu Arquivo




_fileMenu = new FXMenuPane(this);


Cria o painel onde ficarão os itens do menu. Os dois parâmetros do construtor são a janela pai e as opções, que não é necessário informar.




new FXMenuCommand(_fileMenu, "&Carregar imagem...", 0, 0, 0);


Cria um comando de menu. Os principais parâmetros são:

  • Menu onde vai aparecer. Na verdade, pode ficar em um FXComposite qualquer (e, de fato, eu coloquei um sem querer na própria janela, ficou algo bizarro). Como estamos criando menus, é bom que fique em um FXMenuPanel. Neste caso, no menu "Arquivo".
  • Texto do comando. O caractere & indica que a próxima letra ficará sublinhada, para acesso através de Alt+. Neste caso, Alt+C acionará esse comando. É meio Uíndous, mas muito melhor que em Java (um setMnemonic() horroroso).
  • Ícone. FXMenuCommands podem ter um ícone associado, indique aqui. Já falei sobre ícones aqui.
  • Alvo do evento.
  • ID da mensagem. A série sobre Tratamento de Eventos explica essa parte referente a esses dois últimos parâmetros.

O último parâmetro são as opções, novamente desnecessário informar.

E caso não tenha ficado claro pelo nome da classe: FXMenuCommands lançam eventos do tipo SEL_COMMAND. Isso significa que no mapa de mensagens do tratador (seja a própria janela ou seja lá quem for) vai aparecer algo como:



FXMAPFUNC(SEL_COMMAND,
  FoxTutorialMainWindow::ID_LOAD_IMAGE,
  FoxTutorialMainWindow::onCmdLoadImage),



Continuando:



new FXMenuSeparator(_fileMenu);


Apenas um separador. Desenha uma linha para separar os comandos em seções.




new FXMenuCommand(_fileMenu, "&Sair\tCtrl+Q\tEncerrar o aplicativo",
        0, getApp(), FXApp::ID_QUIT);


Mais um comando, para fechar o aplicativo. Desta vez fiz com que esse comando lançasse um evento para exemplificar. Já falei sobre como fechar o aplicativo através de um comando aqui.

A única observação adicional é quanto ao texto do comando. Assim como o texto dos botões, este também é dividido em três seções, separadas pelo caractere de tabulação:

  • A primeira é o texto que será exibido. Natural e praticamente obrigatório.
  • A segunda são as teclas de atalho (aceleradores). Neste caso, coloquei Ctrl+Q para fechar o aplicativo. Opcional.
  • A terceira é o texto que aparecerá na barra de status. Falaremos dela no futuro. Opcional.

Bom, isso preenche o nosso menu "Arquivo". Vamos, agora, adicioná-lo à barra de menus (ou vocês achavam que isso já tinha sido feito?):



new FXMenuTitle(menuBar, "&Arquivo", 0, _fileMenu);


Aqui informamos:

  • A barra de menus onde vai ficar. Novamente, pode ficar em um FXComposite qualquer. Mas não há a menor necessidade de uma bizarrice dessas.
  • O título que vai aparecer. Não preciso mais dizer para que serve o &, preciso?
  • Ícone. Eu, particularmente, nunca vi em nenhum programa (escrito em FOX ou não) um menu com ícone. Mas o FOX Toolkit te dá essa liberdade.
  • O painel que será exibido ao acionar o menu.

Nosso menu Arquivo está pronto. Observemos, agora, o menu Editar.


Menu Editar




_editMenu = new FXMenuPane(this);
  new FXMenuCommand(_editMenu, "&Preferências...", 0, 0, 0);
new FXMenuTitle(menuBar, "&Editar", 0, _editMenu);


Bom, esse aqui é só para mostrar a maneira como eu normalmente crio menus (e, de fato, qualquer objeto composto por uma hierarquia).

Na primeira linha eu sigo o recuo normal do código. Esta é, digamos, a linha de criação (cunhei esse termo agora...).

Em seguida, vêm as linhas de preenchimento (de novo...). Nelas, coloco os objetos que vão "dentro" do objeto principal, com um recuo a mais exatamente para deixar claro essa hierarquia.

No final, volto ao recuo da linha de criação. É a linha de realização ( >D ), pois nela dou um sentido ao meu objeto (afinal, não faz sentido criar um painel sem acesso a ele).

Desenvolvi esse estilo quando comecei a trabalhar com o OpenSceneGraph (não confundam com OpenSG) e precisava definir as características de um modelo, ficava mais ou menos assim:
osg::ref_ptr geode = new osg::Geode();
  osg::Geometry *geometry = new osg::Geometry();
    osg::Vec3Array *vertexArray = new osg::Vec3Array();
      /* Preenche a lista de vértices */
    geometry->setVertexArray(vertexArray);

    osg::DrawElementsUInt *primitiveSet = new osg::DrawElementsUInt(GL_POINTS);
      /* Adiciona as primitivas */
    geometry->addPrimitiveSet(primitiveSet);
  geode->addDrawable(geometry);


Recuar os objetos mais internos me ajudou a entender melhor a hierarquia dos objetos do OpenSceneGraph e acabei aplicando essa técnica ao programar com o FOX Toolkit. Não vou explicar porque está fora do escopo do tópico (e do blog como um todo...).

Bom, é apenas uma sugestão.


Liberando os menus

FXMenuPanels são objetos compartilháveis, assim como ícones. Isso serve, por exemplo, para exibir o mesmo painel pelo menu ou clicando com o botão direito sobre algum objeto.

Sendo assim, devem ser liberados "na mãozona":



delete _fileMenu;
delete _editMenu;



Resultado

Depois disso tudo, o menu Arquivo fica conforme a imagem a seguir. Em destaque, a barra de status mostrando o texto do comando de sair.





Considerações finais

Notem o texto dos comandos de "Carregar imagem..." (menu Arquivo) e "Preferências..." (menu Editar). Eles terminam em reticências.

Terminar um texto de exibição (tanto de comandos de menu como também de botões) com reticências informa imediatamente ao usuário que uma nova janela se abrirá, onde ele terá que executar mais ações.


Outra observação importante é onde posicionar o código para criação dos menus. No início, eu colocava tudo no construtor. As coisas foram aumentando de tamanho, e resolvi apenas criar a barra no construtor e chamar uma função, populateMenu(), para fazer essa tarefa.

Agora, as coisas no meu projeto estão ficando grandes demais, e estou pensando seriamente em criar uma nova classe só para isso. Acontece que, na pressa de obter resultados, a gente acaba tocando tudo no mesmo lugar. Nessa brincadeira até o padrão MVC vai para o espaço. Por favor, não façam isso.


Bom, esse é o que tem de mais básico em termos de menus. Ainda vou discutir sobre submenus e outros elementos que podem aparecer em um FXMenuPanel. Um abraço e até lá.

sexta-feira, 25 de setembro de 2009

FXMessageBox: Tratando a resposta do usuário

Neste tutorial, falei sobre as opções de botões que o FXMessageBox fornece ao usuário. Obviamente, cada mensagem requer um conjunto de botões adequado. Mas como saber qual botão foi pressionado?

Hoje, veremos como detectar qual o botão clicado pelo usuário, em resposta à mensagem exibida.


Relembrando o conjunto de botões

O protótipo das funções que exibem mensagens são da seguinte forma (message, aqui, é utilizado como um nome genérico para information, question, warning e error):
static FXuint message(FXWindow* owner, FXuint opts, const char* caption,
        const char* message, ...);

O parâmetro opts é onde se informa o conjunto de botões que vão aparecer na mensagem, através da seguinte enumeração (abstraindo os valores):
enum {
  MBOX_OK,
  MBOX_OK_CANCEL,
  MBOX_YES_NO,
  MBOX_YES_NO_CANCEL,
  MBOX_QUIT_CANCEL,
  MBOX_QUIT_SAVE_CANCEL,
  MBOX_SKIP_SKIPALL_CANCEL,
  MBOX_SAVE_CANCEL_DONTSAVE
};

Percebam, então, que os botões que podem aparecer em uma mensagem são:
  • OK
  • Cancel
  • Yes
  • No
  • Quit
  • Save
  • Don't save
  • Skip
  • Skip All
Tratando a resposta

Voltando ao protótipo da função, vemos que ela retorna um FXint. É justamente esse retorno que indica qual o botão pressionado. Esse valor também está presente em uma enumeração para facilitar (dar nomes a números mágicos é uma boa prática de programação). Essa enumeração é a seguinte:
enum {
  MBOX_CLICKED_YES      = 1,    /// The YES button was clicked
  MBOX_CLICKED_NO       = 2,    /// The NO button was clicked
  MBOX_CLICKED_OK       = 3,    /// The OK button was clicked
  MBOX_CLICKED_CANCEL   = 4,    /// The CANCEL button was clicked
  MBOX_CLICKED_QUIT     = 5,    /// The QUIT button was clicked
  MBOX_CLICKED_SAVE     = 6,    /// The SAVE button was clicked
  MBOX_CLICKED_SKIP     = 7,    /// The SKIP button was clicked
  MBOX_CLICKED_SKIPALL  = 8     /// The SKIP ALL button was clicked
};

Oito valores enumerados para nove botões?

É...

Fuçando no código fonte, descobri algo assim:
new FXButton(buttons, "&Don't Save", NULL, this, ID_CLICKED_NO, ...

Ou seja, o valor retornado quando o usuário clica em "Don't Save" é MBOX_CLICKED_NO. Faz sentido, mas eu acharia melhor se tivesse um nome específico, o valor até poderia ser o mesmo. Mas vai entender...



Exemplo

Vou dar um exemplo simples, até porque aqui não tem mistério nenhum (fora esse de cima...). Suponha que o usuário vai realizar uma operação que não pode ser desfeita. Isso deve ser informado ao usuário, caso ele volte atrás em sua decisão. Nosso tratador fica assim:
long FoxTutorialMainWindow::onCmdEraseAll(FXObject*, FXSelector sel, void*) {
  FXuint answer;

  answer = FXMessageBox::question(this, MBOX_YES_NO, "Apagar tudo",
      "Essa ação não pode ser desfeita.\nDeseja continuar?");

  switch (answer) {
    case MBOX_CLICKED_YES:
      FXMessageBox::information(this, MBOX_OK, "Dados apagados",
          "Todos os dados foram apagados.");
    break;

    case MBOX_CLICKED_NO:
      FXMessageBox::information(this, MBOX_OK, "Dados não apagados",
          "Os dados não foram apagados.");
    break;

    default: break;
  }

  return 1;
}

Resultado




Discussão

Nada de mais aqui. Se o usuário clicou em "Yes", exibe uma mensagem informando que todos os dados foram apagados. Se clicou em "No", informa que não foram apagados. Realmente sem mistérios.


Conclusão

Este tópico foi apenas um complemento de outro anterior (link). Aqui encerro oficialmente a série sobre o FXMessageBox. A partir daqui, começaremos a falar de elementos que compõem uma interface gráfica típica (menus, barras de ferramentas, barra de status etc.). Até lá!

sexta-feira, 18 de setembro de 2009

Tratamento de Eventos: Sem eventos a tratar

Pode parecer contraditório (e talvez realmente seja) o título deste tópico, mas não vejo onde mais encaixá-lo senão na categoria de Tratamento de Eventos.

Acontece que às vezes criamos uma janela que não precisa tratar nenhum evento em especial, portanto não necessita do mapa de mensagens. É sobre isso que falarei neste tópico.

Quando a janela não trata eventos

Imagine que você precisa de uma janela apenas para exibir algumas informações. Nenhuma entrada do usuário é necessária, nenhuma ação do usuário é executada. Essa janela não precisa tratar evento nenhum, então não precisa de um mapa de mensagens.

Ainda assim, precisa chamar a macro FXIMPLEMENT.


Uma pausa para algumas explicações

Depois de tantos tópicos escritos, só agora senti a necessidade de explicar para que serve esse par de macros: FXDECLARE/FXIMPLEMENT. Confesso que eu simplesmente utilizava, mas sem saber qual o real significado delas. Escrevendo este tópico, tive a curiosidade de investigá-las.

A macro FXDECLARE, como o próprio nome sugere, é colocada dentro da declaração da classe. Após expandida, ela toma a seguinte forma (coloquei espaços para melhorar a legibilidade ):
public:
  struct FXMapEntry {
    FX::FXSelector keylo;
    FX::FXSelector keyhi;
    long (classname::* func)(FX::FXObject*,FX::FXSelector,void*);
  };
  
  static const FX::FXMetaClass metaClass;

  static FX::FXObject* manufacture();

  virtual long handle(FX::FXObject* sender,FX::FXSelector sel,void* ptr);

  virtual const FX::FXMetaClass* getMetaClass() const {
    return &metaClass;
  }

  friend FX::FXStream& operator <<(FX::FXStream& store,const classname* obj) {
    return store.saveObject((FX::FXObjectPtr)(obj));
  }

  friend FX::FXStream& operator >>(FX::FXStream& store,classname*& obj) {
    return store.loadObject((FX::FXObjectPtr&)(obj));
  }

private:

Explicando:
  • Ela cria uma struct interna, chamada FXMapEntry, que armazena as entradas do mapa de mensagens.
  • Declara um objeto estático do tipo FXMetaClass, que armazena informações que descrevem um objeto FOX.
  • manufacture() nada mais é que um método fábrica que retorna um novo objeto; por isso o construtor padrão vazio privado é obrigatório.
  • handle() é o método utilizado para executar as ações propriamente ditas e também utilizado para delegar ações a outros objetos.
  • Os demais são irrelevantes no momento.

Pois bem, a outra macro, FXIMPLEMENT, é o complementar de FXDECLARE. É ela quem vai implementar os métodos declarados por FXDECLARE. Por isso ela é obrigatória, mesmo na ausência do mapa de mensagens. Após expandida, o método handle() é implementado da seguinte maneira:
long classname::handle(FX::FXObject* sender, FX::FXSelector sel, void* ptr) {
  const FXMapEntry* me=(const FXMapEntry*)metaClass.search(sel);

  return me ? (this->* me->func)(sender,sel,ptr)
            : baseclassname::handle(sender,sel,ptr);
}

Fuçando no código-fonte, vi que o método search() faz uma busca linear pelo mapa de mensagens, até encontrar uma entrada contenha o FXSelector sel (passado como parâmetro de handle()). Vejam:
const void* FXMetaClass::search(FXSelector key) const {
  register const FXObject::FXMapEntry* lst=(const FXObject::FXMapEntry*)assoc;
  register FXuint n=nassocs;

  while(n--) {
    if (lst->keylo <= key && key <= lst->keyhi)
      return lst;

    lst = (const FXObject::FXMapEntry*) (((const FXchar*)lst)+assocsz);
  }

  return NULL;
}

Ou seja, se encontrar a entrada, executa a função associada; senão, delega para a superclasse. Isto segue o padrão de projeto "Cadeia de Responsabilidade" ("Chain of Responsibility"), em que um objeto vai delegando a ação a seus superiores (digamos assim), até que alguém o trate. Neste caso, o topo da hierarquia é FXObject, cujo método handle() chama onDefault(), que apenas retorna 0.

Voltando...

Depois de toda essa explicação, podemos voltar ao nosso assunto principal. Suponhamos que temos uma janela que não trata nenhum evento. Neste caso, existem duas opções: declarar um mapa vazio, ou simplesmente não declarar mapa nenhum. A primeira opção já foi discutida em um dos primeiros tutoriais, mas vou reproduzi-la aqui:
FXDEFMAP(FoxTutorialMainWindow) FoxTutorialMainWindowMap[] = {
};

FXIMPLEMENT(FoxTutorialMainWindow, FXMainWindow,
    FoxTutorialMainWindowMap, ARRAYNUMBER(FoxTutorialMainWindowMap))

Neste caso, a macro ARRAYNUMBER calcula o tamanho do vetor, que é 0 (cuidado: alguns compiladores reclamam). Na hora da busca pela mensagem, o laço while é ignorado e a função retorna NULL, disparando o processo já discutido.

A outra maneira é não declarar mapa nenhum. A chamada a FXIMPLEMENT fica assim:
FXIMPLEMENT(FoxTutorialMainWindow, FXMainWindow, NULL, 0)


Conclusão

Acho que este tópico encerra a discussão sobre o mapa de mensagens. Se eu me lembrar de alguma outra coisa nesse sentido, postarei aqui.

Entretanto, ainda há muito o que falar com relação ao tratamento de mensagens. Essa série será bem longa! Até a próxima!

quinta-feira, 17 de setembro de 2009

Tratamento de Eventos: Uma ação para vários IDs

Quando falei sobre o mapa de mensagens (aqui), eu adicionei quatro mapeamentos. Entretanto, todos eles eram do mesmo tipo e chamavam a mesma ação; vejam:

FXMAPFUNC(SEL_COMMAND,
  FoxTutorialMainWindow::ID_INFORMATION,
  FoxTutorialMainWindow::onCmdMessage),

FXMAPFUNC(SEL_COMMAND,
  FoxTutorialMainWindow::ID_QUESTION,
  FoxTutorialMainWindow::onCmdMessage),

FXMAPFUNC(SEL_COMMAND,
  FoxTutorialMainWindow::ID_WARNING,
  FoxTutorialMainWindow::onCmdMessage),

FXMAPFUNC(SEL_COMMAND,
  FoxTutorialMainWindow::ID_ERROR,
  FoxTutorialMainWindow::onCmdMessage),

Uma situação dessas ocorre quando eu tenho várias ações muito semelhantes: em vez de definir um callback para cada ação, eu defino apenas um; a diferença entre eles é feita dentro do corpo da função.

O FOX Toolkit fornece uma macro auxiliar para facilitar essa tarefa: FXMAPFUNCS. Neste tópico, explicarei como usá-la.


Definindo uma ação para vários IDs

Diante de uma situação dessas, seria bem mais fácil se eu pudesse diminuir o número de linhas a serem escritas. A propósito, o FOX Toolkit tem um slogan que diz: "Cada linha de código não escrita é uma linha correta". Pense no caso de uma calculadora: cada botão executa uma ação bem semelhante um ao outro: adicionar o seu valor à expressão e atualizar a caixa de texto que a exibe.

Imagina se eu tivesse que escrever uma função para cada botão? Seriam 10 botões de dígitos + 5 operações (contanto com a igualdade), isso apenas para uma calculadora básica. 15 funções, 15 entradas no mapa.

Para facilitar, pode-se definir apenas uma função e ligá-las todas aos botões da calculadora. E para evitar as quinze entradas no mapa, utiliza-se a macro FXMAPFUNCS. Para não ficar maçante, e reaproveitar os códigos já escritos, vou voltar ao exemplo do tutorial sobre as mensagens exibidas ao usuário.


Declarando as mensagens

Para poder utilizar a macro FXMAPFUNCS, é necessário que os IDs das mensagens sejam declarados seqüencialmente. Isso porque a macro pega um intervalo de IDs. No nosso exemplo:

enum {
  ID_INFORMATION = FXMainWindow::ID_LAST,
  ID_QUESTION,
  ID_WARNING,
  ID_ERROR,

  ID_LAST,
};

Com isso, basta usar a macro mágica. A sintaxe dela é:

FXMAPFUNCS(tipo, ID_min, ID_max, callback)

No nosso exemplo, o mapa de mensagens fica assim:

FXDEFMAP(FoxTutorialMainWindow) FoxTutorialMainWindowMap[] = {
  FXMAPFUNCS(SEL_COMMAND,
             FoxTutorialMainWindow::ID_INFORMATION,
             FoxTutorialMainWindow::ID_ERROR,
             FoxTutorialMainWindow::onCmdMessage),
};

Ou seja, com apenas uma chamada, eu defini a mesma ação para quatro mensagens diferentes.


Diferenciando as mensagens

Já que cada evento vai disparar a mesma ação, é preciso diferenciar os diferentes eventos. Todo callback no FOX Toolkit tem o mesmo formato (com os nomes típicos dos parâmetros):

long onAction(FXObject *sender, FXSelector sel, void *ptr);

Neste caso, o que mais importa é o segundo parâmetro (sempre que um parâmetro não for utilizado, pode permanecer anônimo). Um FXSelector nada mais é que uma mistura entre o tipo de mensagem e o ID da mensagem. Essa mistura é feita automaticamente pela macro FXSEL ao se acrescentar uma entrada no mapa. Apenas adiantando uma informação, essa macro também é utilizada ao se delegar ações; falarei sobre isso no futuro.

Já que o FOX junta essas duas informações, ele pode separá-las: FXSELID recupera o ID da mensagem, e FXSELTYPE recupera o tipo da mensagem. Sendo assim, fica fácil saber quem enviou a mensagem, sem precisar recorrer a ponteiros (como normalmente acontece com uma certa linguagem que dizem não ter ponteiros). Um teste switch resolve o caso:

long FoxTutorialMainWindow::onCmdMessage(
        FXObject*, FXSelector sel, void*) {
  switch (FXSELID(sel)) {
    case ID_INFORMATION:
      FXMessageBox::information(this, MBOX_OK, "Informação",
              "Operação finalizada");
    break;

    case ID_QUESTION:
      FXMessageBox::question(this, MBOX_YES_NO, "Sair",
              "Deseja realmente sair do programa?");
    break;

    case ID_WARNING:
      FXMessageBox::warning(this, MBOX_OK, "Aviso",
              "Valor não especificado.\nAtribuindo padrão 1.");
    break;

    case ID_ERROR:
      FXMessageBox::error(this, MBOX_OK, "Erro",
              "%s: Arquivo não encontrado", filename.text());
    break;

    default: break;
  }

  return 1;
}

Percebam que como eu não uso nem o primeiro nem o último parâmetro, deixei eles anônimos. Dar um nome a eles pode gerar um warning enjoado por parte do compilador.


Conclusão

Neste exemplo, utilizei FXSELID para recuperar o ID da mensagem dentro do FXSelector. Mas eu falei também de FXSELTYPE, para recuperar o tipo. Isso me dá a chance de definir a mesma ação para diferentes tipos e diferenciá-los dentro da função.

Não tenho certeza se dá para fazer algo semelhante no mapa; existem duas macros, FXMAPTYPE e FXMAPTYPES, mas nunca as utilizei. Parece que elas definem um mapeamento de um tipo de mensagem (ou intervalo de tipos, no caso da segunda) para todos os IDs possíveis, chamando a mesma ação. Resumindo: melhor não usar.

Com relação ao mapa de mensagens, acho que só falta falar de quando uma janela não trata nenhum evento. Mas se eu lembrar de mais alguma outra coisa, eu posto aqui. Até a próxima.