XY++
Manual do Usuário
ATENÇÃO: Documentação desatualizada. Veja o código atualizado em "Documentação Interna".
Em seu aspecto global, o XY++ consiste num conjunto de objetos que interagem entre si, respeitando uma hierarquia definida de forma a tornar essas relações mais eficientes, evitando-se ao máximo duplicações e ambiguidades. Observe a hierarquia desses objetos no esquema abaixo.
As classes básicas são XYObject que é a primeira nessa hierarquia e contém o que é comum aos demais membros; XYGraph que é o local onde títulos, eixos, curvas e legenda irão ser representados; XYText que é responsável pelos textos; XYAxis que representa um eixo; XYMask que é responsável pela representação visual de um conjunto de dados; XYSeries que é o próprio conjunto de dados; XYLegend que faz a legenda, XYMarker que define um marcador de região no gráfico e XYPosition que indica a posição absoluta de um objeto ou a relativa a outro objeto. Cada classe será vista com mais detalhes a seguir.
Aplicação
O que se segue de agora em diante é o código-fonte, comentado, da rotina principal da aplicação proposta.
#include "iup.h" // includes da biblioteca IUP #include "cd.h" // includes da biblioteca CD #include "cdiup.h" // includes da biblioteca CD #include "xy.h" // includes da biblioteca XY++ #include "xyserfil.h" // classe derivada da propria aplicacaoApós se incluir as bibliotecas necessárias, começaremos a definir os elementos presentes no gráfico; ou seja, título(s), eixo(s), escala(s), grade(s), curva(s) e legenda.
Para se obter os dois títulos que aparecem no topo do canvas, basta defini-los como feito abaixo:
XYText tipo_de_curva( "Curva de Destilação", XY_BLACK, XYText::large, XYText::helvetica, XYText::italic, XYText::north, XYText::horizontal, true); XYText período( "Amostra do Mês de Junho/96", XY_BLUE, XYText::standard, XYText::timesRoman, XYText::bold, XYText::north, XYText::horizontal);Na sequência, o próximo passo seria definir os dois eixos presentes no gráfico. Proceda como a seguir, observe que os dois eixos definidos são lineares, mas existem outros tipos:
XYLinearAxis eixo_porcentagem( 0.0, 96.0, 0.11, 0.2, XY_RED, 0.7, 0.0, 4., &decorator_porcentagem, &tit_porcentagem); XYLinearAxis eixo_temperatura( (-300, 950, 0.11, 0.2, XY_RED, 0.6, 90.0, 50., &decorator_temperatura, &tit_temperatura);Pode-se verificar que na definição dos eixos, feita acima, aparecem novos dados que precisam ser definidos; quais sejam: o título e o decorador para cada um deles. Para se obter o resultado do exemplo, basta definí-los como segue.
XYText tit_porcentagem ( "Porcentagem (%)", 0.5, 0.15, XY_RED, XYText::standard, XYText::timesRoman, XYText::plain, XYText::center, XYText::horizontal); XYText tit_temperatura ( "Graus Centígrados", 0.03, 0.5, XY_RED, XYText::standard, XYText::timesRoman, XYText::plain, XYText::center, XYText::vertBotTop); XYNumericalScaleDecorator decorador_porcentagem ("%.1f", &escala_porcentagem); XYNumericalScaleDecorator decorador_temperatura ("%.1f", &escala_temperatura);Como se vê, esses decoradores precisam que se defina as escalas onde eles irão atuar. Seguindo o exemplo, considere:
XYText escala_porcentagem( XY_BLACK, XYText::small, XYText::timesRoman, XYText::plain, XYText::north, XYText::horizontal); XYText escala_temperatura( XY_BLACK, XYText::small, XYText::timesRoman, XYText::plain, XYText::east, XYText::horizontal);Definamos, agora, a grade que aparece na região de desenho da curva, observe que esse elemento tem que ser especificado para as duas direções consideradas:
XYGrid grv ( 0., 96., 0., XY_GRAY, 0., 4.); XYGrid grh (-300., 950., -300., XY_GRAY, 90., 50.);A essa altura do exemplo dado, falta apenas definir a máscara (representação da curva que para esse exemplo será adquirida via leitura de um arquivo) e a legenda.
A máscara escolhida é a cartesiana de linha (existem outras) e sua definição é a que se tem abaixo:
XYCartesianLineMask marquivo( &mask_titulo, &arquivo, &eixo_porcentagem, &eixo_temperatura, XY_RED, 1, XYCartesianLineMask::continuous);Observe que a definição acima precisa do acréscimo das especificações de título, eixos e série ao qual esta máscara está relacionada.
A definição do título da máscara pode ser feita assim:
XYText mask_titulo( "Óleo CRU", XY_BLACK, XYText::tile, XYText::timesRoman, XYText::plain, XYText::north, XYText::horizontal);A definição do tipo da série a qual a máscara deve representar é de responsabilidade inteira da aplicação no sentido de que nesse momento se faz uso de uma classe implementada especialmente, pelo usuário, para esse fim. Porém é um passo simples e para o nosso caso essa definição é feita abaixo (código listado mais a adiante), onde o tipo já identifica a série como sendo obtida a partir da leitura de um arquivo cujo nome, no exemplo, é "dado.dat":
XYSeriesFile arquivo ("dado.dat");Finalmente, definimos a legenda. Observe que o nome que aparecerá nela não consta em sua lista de atributos, vem da consulta que ela faz a lista de máscaras envolvidas; no nosso exemplo, apenas um:
XYLegend legend (0.83, 0.3, 1, 1);Por último, vamos definir o tipo de gráfico, dentre os disponíveis, que será desenhado no canvas.
XYCartesian *grafico;Observe-se que a sequência de apresentacão dos elementos envolvidos não se dá necessariamente nessa ordem, ela foi escolhida aqui por ser a mais didática.
Findo o processo de definição dos objetos, passemos às funções úteis a visualização do exemplo dado. Começamos pela função de repaint que, neste caso, é bastante simples:
int frepaint (Ihandle *) { grafico -> clear(); grafico -> draw(); return IUP_DEFAULT; }Nesse nosso caso específico, a função principal seria:
void main(void) { IupOpen (); Ihandle *c = IupCanvas ("arepaint"); Ihandle *d = IupDialog (c); IupSetAttributes (d," SIZE = 400x300, TITLE = XY++"); IupSetFunction ("arepaint", (Icallback) frepaint); IupMap (d); grafico = new XYCartesian (c, 0.0, 0.0, 1.0, 1.0, &grv, &grh); // Insere lista de títulos grafico -> insert (&período); grafico -> insert (&tipo_de_curva); // define cor de fundo para o gráfico grafico -> color (XY_GRAY); // Insere lista de eixos grafico -> insert (&eixo_temperatura); grafico -> insert (&eixo_porcentagem); // define região de desenho para as máscaras baseado nos dois eixos dados grafico -> calcMaskArea (0); // define cor da região de desenho das máscaras grafico -> maskAreaColor (XY_WHITE); // Insere lista de máscaras grafico -> insert (&marquivo); // define a legenda do gráfico grafico -> legend (&legend); // exibe o diálogo que foi definido IupShow (d); // imprime: deixar ativo apenas se a impressão do gráfico for desejada grafico -> print(); // passa o controle para o IUP, interage com o usuário IupMainLoop (); // fecha o IUP IupClose (); }Como foi dito anteriormente, o armazenamento de eixos, curvas (máscaras) e títulos é feito usando-se uma pilha simples, o que precisa do cuidado de se incluir, por exemplo, títulos e curvas na ordem inversa da desejada para que sejam plotadas na ordem correta (inversa a esta) sobre o canvas.
Nesse exemplo, se trocarmos a ordem de inclusão dos títulos colocando o tipo_de_curva acima do período o resultado visual seria aparecer a frase "Amostra do Mês de Junho/96" acima de "Curva de Destilação".
O que vem a seguir são as rotinas auxiliares a implementação desse exemplo, isto é, as que são extensão das já existentes no pacote XY++ (através do mecanismo de herança). Nesse caso específico, a classe que faz a leitura dos dados da série a partir de um arquivo de dados não consta do pacote e seu código fonte é listado.
Na lista de includes desse nosso exemplo aparece "xyserfil.h" que é a classe mencionada no parágrafo anterior e é dela que trataremos a seguir.
Rotinas de Responsabilidade da Aplicação
Veremos agora as rotinas desse nosso exemplo cuja implementação são de responsabilidade exclusiva da aplicação.
Classe que faz leitura de dados a partir de um arquivo
#include "xyser.h" class XYSeriesFile : public XYSeries { public: // construtor da classe XYSeriesFile XYSeriesFile (const char *filename); // destrutor da classe XYSeriesFile virtual ~XYSeriesFile (void); // define o domínio da série inline void domain (double begin, double end); // consulta o domínio da série inline void domain (double& begin, double& end) const; // define o número de pontos dentro do domínio considerado inline unsigned numPoints(void) const; // consulta n-ésimo ponto no domínio inline bool point (unsigned n, double& x, double& y) const; protected: double _begin; // início do domínio double _end; // final do domínio unsigned _first; // índice do primeiro x que está dentro do domínio double *_x, *_y; // coordenadas dos pontos unsigned _np; // número de pontos unsigned _np_dominio; // número de pontos no domínio };Essa classe tem como objetivo a aquisição dos dados da série via leitura de arquivo. Os métodos que ela possui são obrigatórios pois são definidos como virtuais puros na classe-pai "xyser.h" e seus codigos-fonte são listados abaixo:
void XYSeriesFile::domain (double begin, double end) { _begin = begin; _end = end; _first = 0; // atualiza o índice do primeiro x que pertence ao domínio while ( _first < _np && _x[_first] < _begin ) _first++; // índice do último x que está dentro do domínio unsigned last = _first; // atualiza o índice do último x que pertence ao domínio while ( last < _np && _x[last] < _end ) last++; _np_dominio = last - _first; } void XYSeriesFile::domain (double& begin, double& end) const { begin = _begin; end = _end; } unsigned XYSeriesFile::numPoints (void) const { return _np_dominio; } bool XYSeriesFile::point (unsigned n, double& x, double& y) const { if (_np_dominio == 0) return false; if (n >= _np_dominio) return false; n += _first; // desloca para o início do domínio x = _x[n]; y = _y[n]; return true; }Como se pode ver, é uma classe de implementação bastante simples. Observe que seus métodos serão usados para que o XY++ possa identificar o domínio, a quantidade de pontos e a forma de acesso aos dados que devem ser plotados.
Construtor da classe que faz leitura de dados a partir de um arquivo
#include <stdio.h> #include <malloc.h> #include "xyserfil.h" XYSeriesFile::XYSeriesFile (const char *filename) { FILE *f = fopen (filename, "r"); if (f == 0) return; double x, y; unsigned num_alocados = 16; _np = 0; _x = (double *) malloc (sizeof (double) * num_alocados); if (_x == 0) { fclose (f); return; } _y = (double *) malloc (sizeof (double) * num_alocados); if (_y == 0) { free (_x); _x = 0; fclose (f); return; } while (fscanf (f, "%lf %lf", &x, &y) == 2) { if (_np == num_alocados) { num_alocados *= 2; _x = (double *) realloc (_x,sizeof (double) * num_alocados); _y = (double *) realloc (_y,sizeof (double) * num_alocados); } _x[_np] = x; _y[_np] = y; ++_np; } --_np; _first = 0; _begin = _x[0]; _end = _x[_np]; _np_dominio = _np; fclose (f); } XYSeriesFile::~XYSeriesFile (void) { if (_x) free(_x); if (_y) free(_y); }A essa altura nosso objetivo inicial já terá sido atingido. Depois desse exemplo simples, pode-se querer dotar o gráfico já existente de um leque mais robusto de elementos. Neste caso, ainda a título de exemplo, podemos acrescentar algumas funções que nos permitam:
Viabilizar o pick, isto é, termos como feedback de um click sobre um ponto do canvas uma janela com mensagem sobre onde estamos fazendo o click (sobre qual elemento do gráfico). Uma função para isso seria:
int fbutton (Ihandle *, int b, int m, int x, int y, char *) { if (m == 0) // botão solto, retorne return IUP_DEFAULT; cdCanvas2Raster (&x, &y); if (grafico -> pickMask (x, y) != 0) IupMessage ("Pick","Mascara Selecionada"); else if (grafico -> pickAxis (x, y) != 0) IupMessage ("Pick","Eixo Selecionado"); else if (grafico -> pickText (x, y) != 0) IupMessage ("Pick","Texto Selecionado"); else if (grafico -> pickLegend (x, y) != 0) IupMessage ("Pick","Legenda Selecionada"); return IUP_DEFAULT; }No entanto, para que essa função funcione é preciso acrescentar mais duas linha a função main(), isto é, onde antes se via
void main(void) { IupOpen (); Ihandle *c = IupCanvas ("arepaint"); Ihandle *d = IupDialog (c); IupSetAttributes (d," SIZE = 400x300, TITLE = XY++"); IupSetFunction ("arepaint", (Icallback) frepaint); IupMap (d); ... }Agora faça:
void main(void) { IupOpen (); Ihandle *c = IupCanvas ("arepaint"); Ihandle *d = IupDialog (c); IupSetAttributes (d," SIZE = 400x300, TITLE = XY++"); IupSetFunction ("arepaint", (Icallback) frepaint); IupSetAttribute (c, IUP_BUTTON_CB, "fbutton"); // linha nova IupSetFunction ("fbutton", (Icallback) fbutton); // linha nova IupMap (d); ... }Viabilizar o scroll, isto é, podermos mover a curva na direção que desejarmos. Segue aqui as funções para se fazer scroll para a direita e para a esquerda de um passo da grade vertical:
int fscrollright (Ihandle *) // 4. = passo da grade vertical nesse exemplo { arquivo.displacement(4.); grafico -> scroll(grv.sizeWorld2sizePixel(4.), 0); return IUP_DEFAULT; } int fscrollleft (Ihandle *) // 4. = passo da grade vertical nesse exemplo { arquivo.displacement(-4.); grafico -> scroll(-grv.sizeWorld2sizePixel(4.), 0); return IUP_DEFAULT; }Nesse caso, a função repaint muda. Ficando assim:
int frepaint (Ihandle *) { grafico -> setTransformation(); eixo_porcentagem.size(0.7); // tamanho dado a esse eixo quando definido eixo_porcentagem.adjustSize (); grafico -> remove(&eixo_temperatura); grafico -> calcMaskArea (0); grafico -> insert(&eixo_temperatura); grafico -> clear(); grafico -> draw(); return IUP_DEFAULT; }E a função main() precisa do acréscimo de mais quatro linhas para absorver mais esses detalhes. Ou seja, ela passa a ser:
void main(void) { IupOpen (); Ihandle *c = IupCanvas ("arepaint"); Ihandle *d = IupDialog (c); IupSetAttributes (d," SIZE = 400x300, TITLE = XY++"); IupSetFunction ("arepaint", (Icallback) frepaint); IupSetAttribute (c, IUP_BUTTON_CB, "fbutton"); IupSetFunction ("fbutton", (Icallback) fbutton); IupSetAttribute (c, IUP_BUTTON_CB, " fscrollleft "); // linha nova IupSetFunction ("fscrollleft ", (Icallback) fscrollleft); // linha nova IupSetAttribute (c, IUP_BUTTON_CB, " fscrollright "); // linha nova IupSetFunction ("fscrollright ", (Icallback) fscrollright);// linha nova IupMap (d); ... }Uma informação útil é que esse scroll só terá efeito de continuidade sobre a curva se a mesma tiver pontos a acrescentar nas duas direções.
Assim, pode-se ir incluindo cada vez mais elementos na aplicação de forma a satisfazer a necessidade para a qual ela tenha sido idealizada.
Para atingir a intenção de fazer esse manuscrito tão auto-suficiente quanto possível faz-se necessário dar uma idéia da implementação de mais uma das classes de responsabilidade exclusiva da aplicação, mas que aparece no diagrama de hierarquia do XY++. Digamos que a título de curiosidade segue também a listagem da classe "xysersin.h" responsável pela representação da curva seno de um ângulo:
Classe que representa a curva seno de um ângulo
#ifndef __XYSERIESSIN_H #define __XYSERIESSIN_H #include "xyser.h" #include "xymath.h" class XYSeriesSin : public XYSeries { public: // construtor da classe XYSeriesSin XYSeriesSin ( double b, // início do domínio em graus double e, // final do domínio em graus double s) // passo : _begin(b), _end(e), _step(s), _displacement(0.), _np((unsigned) ((e - b) / s)) {}; // destrutor da classe XYSeriesSin virtual ~XYSeriesSin (void){}; // consulta o passo na série inline double step (void) const; //define deslocamento: para testar scroll da região de desenho das // máscaras do gráfico inline void displacement (double d); // define o domínio da série inline void domain (double begin, double end); // consulta o domínio da série inline void domain (double* begin, double* end) const; // consulta o número de pontos dentro do domínio virtual unsigned numPoints (void) const; // consulta o n-ésimo ponto no domínio virtual bool point (unsigned n, double& x, double& y) const; private: double _begin; // início do domínio double _end; // final do domínio double _step; // passo double _displacement; // deslocamento para o scroll unsigned _np; // número de pontos };Essa classe foi desenvolvida para dar o efeito de continuidade quando da aplicação de um scroll. Implemente-a e acrescente-a a aplicação já existente. O código-fonte de seus métodos estão a seguir.
double XYSeriesSin::step (void) const { return _step; } void XYSeriesSin::displacement (double d) { _displacement += d; } void XYSeriesSin::domain (double begin, double end) { _begin = begin; _end = end; _np = (unsigned) ((_end - _begin) / _step); } void XYSeriesSin::domain (double* begin, double* end) const { *begin = _begin; *end = _end; } unsigned XYSeriesSin::numPoints (void) const { return _np; } bool XYSeriesSin::point (unsigned n, double& x, double& y) const { if (_np == 0) return false; if (n > _np) n = 0; x = n * step() + _begin; y = sin((x + _displacement) * PI / 180.0); return true; } #endifObserve que a forma de acesso aos dados que devem ser plotados é diferente aqui, pois o valor de cada ponto não é mais obtido da leitura de uma rquivo e sim da função "seno".
Caso exista interesse nas demais classes presentes no diagrama do XY++ e que são de inteira responsabilidade da aplicação, basta saber que as mesmas são idênticas a anterior alterando-se apenas o valor que a variável "y" receberá. Em outras palavras, temos:
Para "xysercos.h":
inline bool XYSeriesCos::point (unsigned n, double& x, double& y) const { ... y = cos((x + _displacement) * PI / 180.0); return true; }Para "xyserncos.h":
inline bool XYSeriesNCos::point (unsigned n, double& x, double& y) const { ... y = -cos((x + _displacement) * PI / 180.0); return true; }