Você está na página 1de 15

FUNDAÇÃO EDSON QUEIROZ

UNIVERSIDADE DE FORTALEZA - UNIFOR


Centro de Ciências Tecnológica - CCT

DESENVOLVIMENTO DE UM DE SISTEMA DE APOIO A DECISÃO


EM C++ COM INTERFACE GRÁFICA AMIGÁVEL

Francisco Thibério Pinheiro Leitão


Fortaleza, 10/04/2013
1. Introdução

Foi desenvolvido na UNIFOR os três programas baseados nos métodos de Kepner-Tregoe(K-


T), Analitico Hierarquico(AHP), Função de Utilidade(MAUT).

Nosso objetivo aqui será mostrar as principais componentes que compõe o programa, baseado
na teoria apresentada anteriormente. Caso ache queira ver todos os detalhas da implementação,
a documentação do programa pode ser disponibilizada.

Todos os aplicativos foram desenvolvidos em linguagem c++ juntamente com o framework QT


para interface gráfica, por isso muitas classes do framework QT são utilizadas para facilitar a
compatibilidade entre manipulação de arquivos e manipulação da interface gráfica.

A base para toda a estrutura do programa dar-se por meio de manipulação de estruturas de dados
do tipo vetores, matrizes e arvores. Além disso, foi desenvolvida uma classe especializada de
matriz para facilitar o processo de armazenamento e cálculo durante o processo de pontuação.

2. Estrutura Basica

Conforme já foi dito, não será possível detalhar todas as partes que compõe os programas, pois
são muitas linhas de código. Contudo, podemos explicar os processos e as classes que formaram
a base para a construção do sistema mais complexo.

Durante o processo de desenvolvimento dos programas, notou-se que era necessário construir
três estruturas base: Manipulação de Matrizes, Manipulação de Pontuação, Manipulação de
Arquivos. Esquematicamente:
A classe Matriz serve para armazenar dados gerais como nomes e números. Ele serve como
classe base para classe derivada Matriz_SAD que é especializada para fazer certos cálculos
que são utilizados durante o processo de pontuação, por exemplo, média geométrica e pesos
normalizados.

A classe Pontuação_SAD possui a implementação para o calculo da pontuação dos métodos


KT e MAUT. A função de pontuação AHP foi desenvolvida a parte dessa classe e está
implementada como uma função da classe principal main_window no programa do método
AHP.

A classe Manipulação de Arquivos é responsável por salvar e ler todos os dados que foram
utilizados e cálculos durante o processo de uso do programa. Pode então ser dividido em
classes que servem para ler e outras que servem para escrever. Como existem varias estruturas
de dados no programa, iremos mostrar a implementação para matrizes e as outras seguem o
mesmo raciocínio.

3. Manipulação de Matrizes

A classe base para manipulação de Matrizes possui a seguinte definição:

template <class T>


/**
* @brief Classe utilizada como base para manipulacao de elementos em
matrizes. Ele utiliza um vector para armazenar os dados
* em forma de matriz atraves de transformacoes algebricas. Para isso e
necessario saber o numero de colunas e linhas relacionadas
* a este vetor.
*
* A formula utilizada para transfromacao de uma matriz em um vector, para
representacao de dados, e dado por:
* \f$ M_{i,j} = V_{ i * ColumnCount() + j}\f$
*/
class Matriz
{
public:
Matriz();
~Matriz();
Matriz(int Nrows,int NColumns,T value = T());
void SetRowCount(int rows);
void SetColumnCount(int columns);
void Resize(int rows,int columns,T value);
void Resize(int rows,int columns);
void AddRow();
void AddColumn();
int RowCount()const;
int ColumnCount()const;
int size() const;
const T& GetData(int row,int column)const;
void SetData(int row,int column,T value);
const QVector<T>& GetMatriz()const;
const T& At(int row,int column)const;
T& At(int row,int column);
void Clear ( );
bool isEmpty () const;
void Remove ( int row,int column );
const Matriz<T>& operator=(const Matriz<T>& matriz);
T& operator()(int row,int column);
const T& operator()(int row,int column)const;
protected:
int rowCount,columnCount;
QVector<T> matriz;
};

Basicamente ela é responsável por dimensionar o tamanho da matriz, adicionar e remover


dados e saber o número de linhas e colunas.

A classe Matriz_SAD é derivada desta, logo possui todas suas características, além disso
possui funções extras. Abaixo segue a definição da classe:

class Matriz_SAD: public Matriz<double>


{
public:
Matriz_SAD();
~Matriz_SAD();
Matriz_SAD(int Nrows,int Ncolumns,double value = 0);
double GeometricMean(int row) const;
double NormalizedWeight(int row) const;
double ColumnSum(int column) const;
double EigenvalueMax() const;
double RandomicIndex()const;
double CoherenceIndex() const;
double CoherenceReason() const;
double MaxValueRow(int row)const;
double MinValueRow(int row)const;
double MaxValueColumn(int column)const;
double MinValueColumn(int column)const;
double MaxValue()const;
QVector<double> GetRow(int row)const;
QVector<double> GetColumn(int column)const;
};

Cada uma dessas funções é utilizada durante certos processos de cálculos no Sistema de apoio
a decisão. Por exemplo:
double GeometricMean(int row) const;
double NormalizedWeight(int row) const;

São utilizadas durante o processo de calculo de pontuação no método analítico hierárquico. A


primeira retorna a média geometria para uma linha da matriz, em que a linha é referente a
uma determinada alternativa.

double MaxValueRow(int row)const;


double MinValueRow(int row)const;
double MaxValueColumn(int column)const;
double MinValueColumn(int column)const;

São utilizadas durante o processo de construção das funções de utilidade no método MAUT.
Eles retornam o valor máximo ou mínimo para uma determinada linha ou coluna.

Pela teoria, nós sabemos que a média geometria possui a seguinte equação:

𝑁
𝑛
𝑀𝐺𝑗 = √ ∏ 𝐼𝑗𝑘
𝑘=1

Teremos que 𝐼𝑗𝑘 será uma estrutura do tipo matriz e 𝑀𝐺𝑗 será o resultado da média geometrica
referente ao critério j. Esse produtório pode ser calculado por uma estrutura em “for”, conforme
mostrado abaixo:

/**
* @brief Calcula a media geometrica dos valores de uma linha.Utilizado no
metodo AHP para calcular os pesos normalizados.
*
* Utilizada no metodo analitico hierarquico para calcular os pesos
normalizados dos criterios e julgamentos(desempenhos) das alternativas
* relativo a cada criterio.Formula utilizada para calcular:
* \f$ MediaGeometrica_{row} =
{\prod_{j=0}^{Ncolunas}{M_{row,j}}}^{\frac{1}{Ncolunas}}\f$
* Para mais detalhes visitar:
http://www.dss.dpem.tuc.gr/pdf/Decision%20Making%20Guidebook_2002.pdf.
* @param row
* @return double
*/
double Matriz_SAD::GeometricMean(int row) const{
double Product = 1;
double GeometricMean = 0;
double power = 1.0 / (ColumnCount());
for(int j = 0; j<ColumnCount(); j++)
{
if(GetData(row, j) > 0)
{
Product = Product * GetData(row, j);
}
}
GeometricMean = pow(Product, power);
return(GeometricMean);
}

Importante notar que todos os valores da matriz devem ser maiores que zero, senão o
resultado será sempre zero. Isto está de acordo com a teoria, pois os valores podem variar
entre 0.111 e 9. Os valores menores que indicam que o critério é dominado, conforme teoria.

Os pesos normalizados possuem a seguinte equação teórica:

𝑀𝐺𝑗
𝑃𝑁𝑗 = 𝑁
∑𝑗=1 𝑀𝐺𝑗

Em que 𝑀𝐺𝑗 é a media geométrica referente ao critério je 𝑃𝑁𝑗 seu peso normalizado. A
implementação feita da seguinte forma:

/**
* @brief Cacula o peso normalizado dos valores de uma linha.Utilizado no
metodo AHP durante o processo de pontucao.
*
* Calculado como a media geometrica divido pela soma das medias
geometricas.
* @param row
* @return double
*/
double Matriz_SAD::NormalizedWeight(int row) const{
double NormalizedWeight;
double Sum_GeometricMean = 0;
for(int i = 0; i<RowCount(); i++)
{
Sum_GeometricMean = Sum_GeometricMean + GeometricMean(i);
}
NormalizedWeight = GeometricMean(row) / Sum_GeometricMean;
return NormalizedWeight;
}

O calculo do valor máximo de uma linha na matriz é utilizado durante o processo de


construção das funções de interpolação de Lagrange(Funções de Utilidade). A
implementação é dada por:

/**
* @brief Calcula o valor maximo existente em uma determinada linha da
matriz.Utilizado no metodo MAUT.
*
* Utilizada para criar as funcoes de utilidade no metodo MAUT. Neste caso,
a linha representa um criterio, e a matriz
* armazena os valores respectivos a cada criterio.
* @param row
* @return double
*/
double Matriz_SAD::MaxValueRow(int row)const{
QVector<double> row_values = GetRow(row);
return *max_element(row_values.begin(),row_values.end());
}

Como já existe um função padrão para calcular o valores máximos em vetores, apenas
chamamos essa função para uma determinada linha da nossa matriz.

4. Manipulação de Pontuação

A pontuação no método AHP foi implementada independente das outras. A classe para
manipulação de pontuação possui a seguinte definição:

class Pontuacao_SAD
{
public:
Pontuacao_SAD();
double PontuacaoKT(int alternativa_i,const QVector<Matriz_SAD>&
Julgamento,QTreeWidget* Criterio);
double PontuacaoKT(int alternativa_i,const QVector<Matriz_SAD>&
Julgamento,QTreeWidget* Criterio
,int criteriopai,int criterio_filho,double peso);
double PontuacaoKT(int alternativa_i,const QVector<Matriz_SAD>&
Julgamento,QTreeWidget* Criterio
,int criteriopai,double peso);

double PontuacaoMAUT(int alternativa_i,const QVector<Matriz_SAD>&


Julgamento,QTreeWidget* Criterio);
double PontuacaoMAUT(int alternativa_i,const QVector<Matriz_SAD>&
Julgamento,QTreeWidget* Criterio
,int criteriopai,int criterio_filho,double peso);
double PontuacaoMAUT(int alternativa_i,const QVector<Matriz_SAD>&
Julgamento,QTreeWidget* Criterio
,int criteriopai,double peso);
};

Temos três métodos para pontuação para cada método, sendo que dois deles possuem
pequenas modificações que possibilitam a construção dos gráficos. Vamos detalhar o significa
de cada parte:

double PontuacaoKT(int alternativa_i,const QVector<Matriz_SAD>&


Julgamento,QTreeWidget* Criterio);

O primeiro valor identifica para qual alternativa a pontuação esta sendo calculada, o segundo
valor identifica as matrizes de julgamentos e o terceiro contem os pesos de cada critério.
Esses são os dados necessários para se calcular a pontuação no método K-T.
Além disso, foi implementado considerando que é possível a divisão de critérios em grupos.
O calcula da pontuação pode então ser dividida em duas partes, primeiro calcula-se a
pontuação da alternativa relativa a cada grupo:

𝑃(𝐴𝑖 , 𝑘) = ∑ 𝑃𝑗𝑘 × 𝑘𝐽𝑗𝑖


𝑗=1

Depois soma-se todas as pontuação com seus respectivos pesos:

𝑃(𝐴𝑖 ) = ∑ 𝑃𝐺 𝑘 × 𝑃(𝐴𝑖 , 𝑘)
𝑘=1

A estrutura de dados do tipo arvore QTreeWidget* Criterio é responsável por armazenar


os grupos e seus respectivos critérios.

A estrutura de dados do tipo arvore QVector<Matriz_SAD>& Julgamento é equivalente


matematicamente as matrizes 𝑘𝐽𝑗𝑖 .

É possível notar que a grande diferença entre as equações e a implementação está nas
estruturas de dados utilizadas para armazenar os valores.

/**
* @brief Pontuacao de Kepner-Tregoe quando a definicao de criterios e dado
por uma QTreeWiget com 1 level.
*
* Os pesos dos criterios estao definidos na segunda coluna da arvore. Para
cada criterio pai, podemos ter N criterio filhos. Isso
* e calculado pela funcao childCount() para cada criterio pai.
* Alem disso, para cada criterio pai, teremos que fazer os Julgamentos de
desempenho de acordo com esquema basico do metodo de
* Kepner-Tregoe. Com essa matriz dada, podemos entao calcular a pontuacao
de qualquer alternativa pela formula:
* \f$ PA_i = \sum_{k=0}^N(PCP_k\sum_{j=0}^M(J^{k}_{i,j}PCF^{k}_{j}))\f$.
*
* @param alternativa_i
* @param Julgamento
* @param Criterio
* @return double
*/
double Pontuacao_SAD::PontuacaoKT(int alternativa_i,const
QVector<Matriz_SAD>& Julgamento,QTreeWidget* Criterio){
double Pontuacao_Alternativa_i = 0;
QVector<double> Soma(Criterio->topLevelItemCount(),0);

for(int k=0;k<Criterio->topLevelItemCount();++k){
for(int j=0;j<Criterio->topLevelItem(k)->childCount();++j){
Soma[k] = Soma.at(k) +
Julgamento.at(k).At(alternativa_i,j)*Criterio->topLevelItem(k)->child(j)-
>data(1,Qt::DisplayRole).toDouble();
}

Pontuacao_Alternativa_i += Soma.at(k)*Criterio->topLevelItem(k)-
>data(1,Qt::DisplayRole).toDouble();
}

return Pontuacao_Alternativa_i;
}

A analise de implementação de estruturas em “for” é feita de dentro para fora, ou seja, o


segundo “for” é responsável pelo calculo da primeira equação e o primeiro “for” é
responsável pelo calculo da segunda equação. As outras duas funções de pontuação para K-T
são derivadas dessa, mas são feitas para a analise gráfica. Podem ser desenvolvidas
considerando um determinado valor deve ser considerado como variável. Ou seja, para o caso
simples:

𝑃(𝐴𝑖 , 𝑃𝑘 ) = ∑ 𝑃𝑗 × 𝐽𝑗𝑖 + 𝑃𝑘 × 𝐽𝑘𝑖


𝑗=1,𝑗≠𝑘

Em que 𝑃𝑘 é o valor variável adicional que deve ser passado para função.

Vamos considerar agora os cálculos para funções de pontuação no método MAUT. Devido a
complexidade do processo, vamos dividir em passos:

- Dados necessários para calcular a pontuação

double Pontuacao_SAD::PontuacaoMAUT(int alternativa_i,const


QVector<Matriz_SAD>& Julgamento,QTreeWidget* Criterio){
QVector<double> PesoTotalCF;
double Pontuacao_Alternativa_i = 0;
double PesoTotalCP = 0;
QVector<double> Soma(Criterio->topLevelItemCount(),0);

- Primeiro criar a função de pontuação(Interpolação Lagrange):

Interpolacao_Lagrange Funcao_Pontuacao;
Funcao_Pontuacao.AddPoint(1,0);
Funcao_Pontuacao.AddPoint(2,0.25);
Funcao_Pontuacao.AddPoint(3,0.5);
Funcao_Pontuacao.AddPoint(4,0.75);
Funcao_Pontuacao.AddPoint(5,1.0);

-Somar os pesos critérios pai(ou Grupos) e critérios(ou Critérios Filho)

for(int k=0;k<Criterio->topLevelItemCount();++k){ /**< Normalizar os pesos


dos criterios pais */
PesoTotalCP = PesoTotalCP + Criterio->topLevelItem(k)-
>data(1,Qt::DisplayRole).toDouble();
}
for(int k=0;k<Criterio->topLevelItemCount();++k){ /**< Normalizar os
pesos dos criterios filhos,para cada criterio pai */
PesoTotalCF[k] = 0;
for(int j=0;j<Criterio->topLevelItem(k)->childCount();++j){
PesoTotalCF[k] = PesoTotalCF.at(k) + Criterio->topLevelItem(k)-
>child(j)->data(1,Qt::DisplayRole).toDouble();
}
}

- Normalizar os pesos e Calcular a pontuação da alternativa


for(int k=0;k<Criterio->topLevelItemCount();++k){
double PesoNormalizadoCP = Criterio->topLevelItem(k)-
>data(1,Qt::DisplayRole).toDouble()/PesoTotalCP;

for(int j = 0; j<Criterio->topLevelItem(k)->childCount();++j){
double PesoNormalizadoCF = Criterio->topLevelItem(k)->child(j)-
>data(2,Qt::DisplayRole).toDouble()/PesoTotalCF.at(k);
Soma[k] = Soma.at(k) +
PesoNormalizadoCF*Funcao_Pontuacao(Julgamento.at(k).At(alternativa_i,j));
}
Pontuacao_Alternativa_i += PesoNormalizadoCP*Soma.at(k);
}

Conforme foi dito, a analise de estruturas em “for” deve ser feita de dentro para forma, para
se identificar os termos na equação.

O segundo “for” do calculo acima corresponde ao calculo da pontuação “parcial”


𝑁

𝑃(𝐴𝑖 , 𝑘) = ∑ 𝑃𝑁𝑗𝑘 × 𝐹( 𝑘𝐽𝑗𝑖 )


𝑗=1

O primeiro “for” corresponde a soma das pontuações “parciais”

𝑃(𝐴𝑖 ) = ∑ 𝑃𝐺 𝑘 × 𝑃(𝐴𝑖 , 𝑘)
𝑘=1

Em que:

PesoNormalizadoCP: 𝑃𝐺 𝑘

𝑘 𝑖
Julgamento.at(k).At(alternativa_i,j): ( 𝐽𝑗 )

Funcao_Pontuacao: 𝐹

PesoNormalizadoCF: 𝑃𝑁𝑗𝑘
5. Manipulação de Arquivos

Será apresentado as funções que são utilizadas para salvar os dados utilizados nos programas.
As principais estruturas de dados utilizadas no programa são tabelas, textos e arvores. Logo
será apresentado as funções utilizadas para ler e escrever esses dados.

Abaixo segue a definição das funções para matrizes, arvores e textos respectivamente:

void WriteMatriz(const Matriz_SAD& matriz,QTextStream& write_file);

void ReadMatriz(Matriz_SAD& matriz,QTextStream& read_file);

void WriteTree(QTreeWidget* arvore,QTextStream& write_file);

void ReadTree(QTreeWidget* arvore,QTextStream& read_file);

void WritePlaintTextTable(QStandardItemModel* table,QTextStream& write_file);

void ReadPlaintTextTable(QStandardItemModel* table,QTextStream& read_file);

Os arquivos são manipulados no formato CSV(Comma-separated values) , ou seja, os dados


são separados por ponto e virgula.

Abaixo segue a implementação deste tipo de formatação para escrever e ler os dados de uma
matriz em um arquivo texto. A ideia é você escrever de uma modo simples e depois
simplesmente ler os dados exatamente na ordem como você salvou:

/**
* @brief Escreve em um arquivo ligado a QTextStream, os dados de uma
matriz de sistema de apoio a decisao na forma csv.
*
* @param matriz
* @param write_file
*/
void Manipulacao_Arquivos::WriteMatriz(const Matriz_SAD&
matriz,QTextStream& write_file){
write_file<<matriz.RowCount()<<";"<<matriz.ColumnCount()<<endl;
for (int i = 0; i < matriz.RowCount(); ++i) {
if(i>0){
write_file<<endl;
}
for (int j = 0; j < matriz.ColumnCount(); ++j) {
if(j>0){
write_file<<";";
}
write_file << matriz.GetData(i,j);
}
}
if(matriz.RowCount()>0 && matriz.ColumnCount()>0){
write_file<<endl;
}
}

/**
* @brief Le de um arquivo ligado a QTextStream, os dados de uma matriz de
sistema de apoio a decisao na forma csv.
*
* So existem dados em uma tabela, ou matriz, caso o numero de colunas e
linhas seja maior que zero, por isso e necessario fazer
* esta verificao durante a leitura.
* @param matriz
* @param read_file
*/
void Manipulacao_Arquivos::ReadMatriz(Matriz_SAD& matriz,QTextStream&
read_file){
matriz.Clear();
QStringList strlist;
strlist = read_file.readLine().split(";");
int nlinhas = strlist.at(0).toInt();
int ncolunas = strlist.at(1).toInt();
matriz.Resize(nlinhas,ncolunas);
if(nlinhas>0 && ncolunas>0){
for (int i = 0; i < nlinhas; ++i) {
strlist.clear();
strlist = read_file.readLine().split(";");
for (int j = 0; j < ncolunas; ++j) {
matriz.SetData(i,j,strlist.at(j).toDouble());
}
}
}
}

6. Interpolação de Lagrange

A interpolação de Lagrange consiste em construir uma função que passe por um conjunto de
valores predefinidos. Isso significa que dado um conjunto de pontos {𝑥𝑖 , 𝑦𝑖 }𝑁
𝑖=1 a função de

interpolação deve passar exatamente por todos os pontos e possui grau 𝑁 − 1.

Essa é uma função base para construção de um programa que utilizada o método das funções
de utilidade, pois essas são construídas por funções de interpolação de lagrange. No programa
que utiliza o método MAUT for considerado somente funções lineares para as funções de
utilidade.

Essas funções podem ser construídas através das funções base de Lagrange para cada ponto:

𝑁
𝑥 − 𝑥𝑘
𝑙𝑗 (𝑥) = ∏
𝑥𝑗 − 𝑥𝑘
𝑘=1,𝑘≠𝑗
É fácil verificar que essas funções satisfazem a seguinte condição:

𝑙𝑗 (𝑥𝑗 ) = 1

𝑙𝑗 (𝑥𝑘 ) = 0 ,𝑘 ≠ 𝑗

Devido a essas simples, porém importantes, propriedades é possível construir uma função
polinomial que passa exatamente em cada um desses pontos através da seguinte combinação
linear:

𝜑(𝑥) = ∑ 𝑦𝑖 𝑙𝑖 (𝑥)
𝑖=1

Considerando essas equações, precisamos programar em um computador. Primeiramente é


necessário armazenar o conjunto de pontos, depois através de dois loop’s em “for” nós
calculamos as funções base e o somatório final. Abaixo segue a definição da classe:

class Interpolacao_Lagrange
{
public:
Interpolacao_Lagrange();
~Interpolacao_Lagrange();
Interpolacao_Lagrange(const QVector<Point>& p);
Interpolacao_Lagrange(const QVector<double> x,const QVector<double> y);
void SetPoints(const QVector<Point>& pts);
void SetPoints(const QVector<double> x, const QVector<double> y);
void AddPoint(const Point& pt);
void AddPoint(double x, double y);
void RemovePoint(const Point& pt);
void RemovePoint(double x, double y);
void Clear();
int PointCount()const;
const QVector<Point>& GetPoints()const;
const QVector<double> GetXValues()const;
const QVector<double> GetYValues()const;
double CalculateValue(double x)const;
QVector<double> CalculateValue(const QVector<double>& x)const;
double operator()(double x)const;
QVector<double> operator()(const QVector<double>& x)const;
const Interpolacao_Lagrange& operator=(const Interpolacao_Lagrange&
objeto);
private:
QVector<Point> points;
};

As maiorias das funções são para permitir a manipulação dos dados privados da classe. A
função que calcula o valor da função é definida abaixo:
/**
* @brief Calcula o valor interpolado de x, substituindo na Funcao de
lagrange.
*
* Os calculos sao feitos atraves da formula basica de lagrange para
interpolacao. Para maiores detalhes
* visitar o site: http://en.wikipedia.org/wiki/Lagrange_polynomial.
* @param x
* @return double
*/
double Interpolacao_Lagrange::operator()(double x)const{
QVector<double> Base_function(PointCount(),1);
double Sum = 0;
for(int j=0;j<PointCount();j++){
for(int i=0;i<PointCount();i++){
if(i!=j){
Base_function[j] = Base_function.at(j)*(x-
points.at(i).X())/(points.at(j).X()-points.at(i).X());
}
}
Sum = Sum + points.at(j).Y()*Base_function.at(j);
}
return Sum;
}

O segundo “for” (interno) calcula as funções base de Lagrange , enquanto que o primeiro”
for”(externo) interpola os valores das funções.
Referencias

Qt Project. Model/View Programming. Disponível em <http://qt-project.org/doc/qt-


4.8/model-view-programming.html >.

Cplusplus. Polymorphism. Disponível em <


http://www.cplusplus.com/doc/tutorial/polymorphism/ >>, acessado em 4/2013.

Cplusplus. C++ STL. Disponível em < http://www.cplusplus.com/reference/algorithm/ >>,


acessado em 4/2013.

Wikipedia. Comma-separated values. Disponível em < http://en.wikipedia.org/wiki/Comma-


separated_values >, acessado em 4/2013.

C++ Operator Overloading Guidelines. Disponível em <


http://courses.cms.caltech.edu/cs11/material/cpp/donnie/cpp-ops.html >, acessado em 4/2013.

Qt plotting widget. Disponível em


http://www.workslikeclockwork.com/index.php/components/qt-plotting-widget/, acessado em
4/2013.

Você também pode gostar