SlideShare uma empresa Scribd logo
1 de 27
Baixar para ler offline
4. Implementando Funcionalidade da Aplicação
• A Widget Central
• A Subclasse QTableWidget
• Carregando e Salvando
• Implementando o Menu Editar
• Implementando os Outros Menus
• A Subclasse QTableWidgetItem
Nos dois últimos capítulos, explicamos como criar a Interface de Usuário para a
aplicação com Planilha. Neste capítulo, vamos terminar o programa codificando
suas funcionalidades básicas. Entre outras coisas, veremos como carregar e salvar
arquivos, como armazenar dados na memória, como implementar operações em
Área de Transferência, e como adicionar suporte às fórumas de planilhas para o
QTableWidget.
O Widget Central
A área central de uma QMainWindow pode ser ocupado por qualquer tipo de Widget.
Eis uma visão geral das possibilades:
1. Use uma Qt Widget Padrão.
Uma widget padrão como a QTableWidget ou a QTextWidget podem ser
usadas como Widgets centrais. Neste caso, a funcionalidade da aplicação, como
carregar e salvar arquivos, devem ser implementados nos demais locais( em uma
subclasse QMainWindow, por exemplo).
2. Use uma Qt Widget Personalizada.
Aplicações especializadas freqüentemente necessitam mostrar dados em
uma Widget Personalizada. Por exemplo, um programa editor de ícones teria um
Widget IconEditor atuando como o Widget central. O capítulo 5 explica como
escrever Widgets Personalizados em Qt.
3. Use uma Qt Widget limpa com um gerenciador de layouts.
Ás vezes a área central da aplicação é ocupada por diversos
widgets. Isso pode ser feito usando um QWidget como um pai de
todos os demais widgets, e gerenciadores de layout para ajustar
tamanho e posição das Widgets filhas.
4. Use um separador.
Outra maneira de se usar diversas widgets juntas é utilizando um QSplitter.
O QSplitter ordena suas widgets-filhas horizontal e verticalmente, com
sontroladores de separação que dão um certo controle de tamanho ao
usuário. Ordenadores podem conter todos os tipos de widgets, incluindo
outros splitters.
5. Use uma área MDI.
Se a aplicação usa MDI, a área central é ocupada por um QMdiArea Widget, e
cada Janela MDI é uma instância daquela widget.
Layouts, separadores e áreas MDI podem ser combinadas com Qt Widgets padrão
ou com widgets customizados. O Capítulo 6 aborda essas classes com mais
detalhes.
Para a aplicação da Planilha, uma subclasse QTableWidget é usada como a widget
central. A classe WTableWidget já possui a maior parte da capacidade da planilha de
que precisamos, mas não suporta operações na área de transferência e não
processa fórmulas de planilhas como “=A1+A2+A3”. Vamos precisar implementar
essa funcionalidade que está faltando, na classe Spreadsheet.
A Subclasse QTableWidget
A Classe Planilha é derivada de QTableWidget, como mostra a figura 4.1. Uma
QTableWidget é efetivamente uma grade que representa uma matriz esparsa
bidimensional. Ela mostra todas as células para as quais o usuário se direcione,
com suas respectivas dimensões. Quando o usuário entra algum texto em uma
célula vazia, QTableWidget automaticamente cria um QTableWidgetItem para
armazenar o próximo.
Figura 4.1. Árvores de Herança para Planilha e Célula
QTableWidget é derivado de QTableView, uma das classes de
modelagem/visualização que analisaremos mais de perto no Capítulo 10. Outra
tabela, que possui muito mais funcionalidades fora da caixa, é a QicsTable,
disponível em Http://www.ics.com.com/.
Vamos iniciar implementando Spreadsheet, começando pelo arquivo cabeçalho:
#ifndef SPREADSHEET_H
#define SPREADSHEET_H
#include <QTableWidget>
class Cell;
class SpreadsheetCompare;
O cabeçalho inicia com declarações para as classes Cell e SpreadsheetCompare
Os atributos de uma célula de uma QTableWidget, como texto e alinhamento, são
armazenados em uma QTableWidgetItem. Diferente de QTableWidget, a
QTableWidgetItem não é uma classe widget; é puramente uma classe de dados. A
classe Cell é derivada de QTableWidgetItem e serão explicados na última sessão
deste capítulo.
class Spreadsheet : public QTableWidget
{
Q_OBJECT
public:
Spreadsheet(QWidget *parent = 0);
bool autoRecalculate() const { return autoRecalc; }
QString currentLocation() const;
QString currentFormula() const;
QTableWidgetSelectionRange selectedRange() const;
void clear();
bool readFile(const QString &fileName);
bool writeFile(const QString &fileName);
void sort(const SpreadsheetCompare &compare);
A função autoRecalculate() implementada internamente já que ela apenas retorna
se o auto-recálculo está “in force” ou não.
No Capítulo 3, nós dependemos de algumas funções públicas na classe
Spreadsheet quando implementamos MainWindow. Por exmplo, chamamos clear()
de MainWindow::newFile() para resetar a planilha. Nós também usamos algumas
funções herdadas de QTableWidget, como setCurrentCell() e setShowGrid().
public slots:
void cut();
void copy();
void paste();
void del();
void selectCurrentRow();
void selectCurrentColumn();
void recalculate();
void setAutoRecalculate(bool recalc);
void findNext(const QString &str, Qt::CaseSensitivity cs);
void findPrevious(const QString &str, Qt::CaseSensitivity cs);
signals:
void modified();
Spreadsheet fornece vários slots que implementam ações dos menus Edit, Tools e
Options, e também usa um sinal, modified(), para anunciar quando alguma
mudança ocorrer.
Private slots:
Void somethingChanged();
Definimos um slot privado usado internamente pela classe Spreadsheet.
private:
enum { MagicNumber = 0x7F51C883, RowCount = 999, ColumnCount = 26 };
Cell *cell(int row, int column) const;
QString text(int row, int column) const;
QString formula(int row, int column) const;
void setFormula(int row, int column, const QString &formula);
bool autoRecalc;
};
Na seção provada da classe, declaramos três constantes, quatro funções, e uma
variável.
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
};
#endif
O cabeçalho termina com a definição da classe SpreadsheetCompare. Explicaremos
isto quando revisarmos Spreadsheet::sort().
Vamos agora dar uma olhada na implementação:
#include <QtGui>
#include "cell.h"
#include "spreadsheet.h"
Spreadsheet::Spreadsheet(QWidget *parent)
: QTableWidget(parent)
{
autoRecalc = true;
setItemPrototype(new Cell);
setSelectionMode(ContiguousSelection);
connect(this, SIGNAL(itemChanged(QTableWidgetItem *)),
this, SLOT(somethingChanged()));
clear();
}
Normalmente, quando o usuário entra com texto em uma célula vazia, o
QTableWidget automaticamente criará um QTableWidgetItem para guardar o texto.
Na nossa planilha, queremos que sejam criados itens de Cell no lugar. Isso é
possível graças a chamada de setItemprototype() no construtor. Internamente,
QTableWidget duplica o item passado como protótipo toda vez que um novo item é
necessário.
Ainda no construtor, setamos o modo de seleção para
QAbstractItemView::ContiguousSelection para permitir uma seleção retangular
única. Conectamos o sinal itemChanged() da widget da tabela para o slot privado
somethingChanged(); isso assegura que quando o usuário editar uma célula, o slot
somethingChanged() é chamado. Finalmente, chamamos clear() para
redimensionar a tabela e ajustar os cabeçalhos das colunas.
void Spreadsheet::clear()
{
setRowCount(0);
setColumnCount(0);
setRowCount(RowCount);
setColumnCount(ColumnCount);
for (int i = 0; i < ColumnCount; ++i) {
QTableWidgetItem *item = new QTableWidgetItem;
item->setText(QString(QChar('A' + i)));
setHorizontalHeaderItem(i, item);
}
setCurrentCell(0, 0);
}
A função clear() é chamada do construtor Spreadsheet para inicializar a planilha.
Ele também é chamado de MainWindow::newFile().
Poderíamos ter usado QTableWidget::clear() para limpar todos os itens e
quaisquer seleções, mas isto deixaria os cabeçalhos em seus tamanhos atuais. Ao
invés disso, redimensionamos a tabela para 0 x 0. Isso limpa a planilha inteira,
incluindo os cabeçalhos. Depois redimensionamos a tabelas para ColumnCount x
RowCount (26 x 999) e povoamos o cabeçalho horizontal com itens de
QTableWidgetItem contendo os nomes “A”, “B”, ..., “Z”. Não precisamos ajustar os
campos dos cabeçalhos verticais, já que estes possuem valores-padrão “1”,
”2”, ...,”999”. No final, movemos o cursor para a célula A1.
Um QTableWidget é composto de diversos widgets-filhos. Tem um QHeaderView
horizontal no topo, um QHeaderView vertical no lado esquerdo, e dois widgets
WScrolBar. A área no meio é ocupada por um widget especial chamado de
viewport, no qual QTableWidget desenha as células. Os diferentes widgets-filhos
são acessíveis através das funções herdadas de QTableWidget e
QAbstractScrollArea ( ver Figura 4.2). QAbstractScrollArea fornece um
viewport rolável e duas barras de rolagem, que podem ser ligadas e desligadas.
Veremos mais da subclasse QScrollArea no Capítulo 6.
Figura 4.2: Widgets que constituiem QTableWidget
Cell *Spreadsheet::cell(int row, int column) const
{
return static_cast<Cell *>(item(row, column));
}
A função cell() returna o Objeto Cell para uma linha e coluna dadas. É quase o
mesmo de QTableWidget::Item(). Exceto que retorna um ponteiro para Cell ao
invés de um ponteiro para QTableWIdgetItem.
QString Spreadsheet::text(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->text();
} else {
return "";
}
}
A função text(), privada, retorna o texto para uma dada célula. Caso cell() retorne
um ponteiro null, a célula é vazia, então nós retornamos uma string vazia.
QString Spreadsheet::formula(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->formula();
} else {
return "";
}
}
A função formula() retorna a fórmula da célula. Em muitos casos, a fórmula e o
texto são os mesmos; por exemplo, a fórmula “Hello” equivale à string “Hello”,
então se o usuário digita “Hello” em uma célula e aperta Enter, a célula mostra o
texto “Hello”. Há algumas exceções, porém:
• Caso a fórmula seja um número, é interpretada como tal. Por exemplo, a
fórmula “1.50” equivale ao valor do tipo Double 1.5, que é passado como um
valor alinhado à direita “1.5” na planilha.
• Caso a fórmula inicie com aspas simples, o restante da fórmula é
interpretado como texto. Por exemplo, a fórmula “’12345’” equivale à string
“12345”.
Armazenando Dados como Itens
Na aplicação da Planilha, cada célula não-vazia é armazenada em memória como
um Objeto QListWidgetItem individual. Armazenar dados como itens é uma
abordagem que é usado inclusive por QListWidget e TreeWidget, o qual opera em
elementos de QListWidgetItem e QtreeWidgetItem.
Classes de itens do Qt possuem uma função a mais, armazenando mais
informações. Por exemplo, um QTableWidgetItem já armazena alguns atributos,
incluindo string, cor de fonte, ícone, e um ponteiro para QTableWIdget. Os itens
podem também armazenar dados (QVariantS), incluindo tipos personalizados
registrados, e através da herança da classe deste item, podem-se fornecer
funcionalidades adicionais.
Kits de ferramentas mais antigos possuem um ponteiro do tipo void em suas
classes de itens para armazenar dados adicionais. No Qt, a ação mais natural é
usar setData() com uma QVariant, mas se um ponteiro para void for necessário,
pode ser obtido de forma trivial herdando uma classe de item e adicionando uma
variável do tipo ponteiro para void.
Para requisitos mais complexos de controle de dados, como grande data sets,
classes de itens complexas, integração de banco de dados e visualizações
múltiplas de dados, o Qt fornece uma série de classes de modelo e visualização
que separam a data de sua representação visual. Esse assunto será abordado no
Capítulo 10.
• Caso a fórmula inicie com um sinal de igual (‘=’), a fórmula é interpretada
como uma fórmula aritmética. Por exemplo, se a célula A1 contém “12” e a
célula A2 contém“6”, a fórmula “=A1+A2” retorna 18.
A tarefa de converter uma fórmula em um valor é realizado pela classe Cell. No
momento, o que deve se manter em mente é que o texto exibido na célula é o
resultado da fórmula, e não a fórmula em si.
void Spreadsheet::setFormula(int row, int column,
const QString &formula)
{
Cell *c = cell(row, column);
if (!c) {
c = new Cell;
setItem(row, column, c);
}
c->setFormula(formula);
}
A função privada setFormula() habilita a fórmula para uma dada célula. Caso a
célula já possua um objeto Cell, deve-se reutilizá-la. Caso contrário, criamos um
novo objeto do tipo Cell e chamamos QTableWidget::setItem() para inseri-la
dentro da tabela. No final, chamamos a própria função setFormula() da célula, que
fará com que a célula seja remodelada caso seja mostrada na tela. Não precisamos
nos preocupar em deletar o objeto Cell mais tarde; QTableWidget toma posse da
célula e irá deletá-la automaticamente na hora certa.
QString Spreadsheet::currentLocation() const
{
return QChar('A' + currentColumn())
+ QString::number(currentRow() + 1);
}
A função currentLocation() retorna a localização atual da célula no formato usual
da planilha, coluna de letras seguido por linhas enumeradas.
MainWindows::updateStatusBar() o usa para mostrar a localização na barra de
status.
QString Spreadsheet::currentFormula() const
{
return formula(currentRow(), currentColumn());
}
A função currentFormula() retorna a fórmula da célula passada. Ela é chamada
em MainWindow::updateStatusBar().
void Spreadsheet::somethingChanged()
{
if (autoRecalc)
recalculate();
emit modified();
}
O slot privado somethingChanged() recalcula a planilha inteira caso
“auto-recalculate” esteja habilitado. Emite um sinal modified().
Carregando e Salvando
Vamos agora implementar a opção de Salvar e Carregar em arquivos da Planilha
utilizando um formato binário personalizado. Faremos isto usando QFile e
QDataStream, que juntos fornecem Entrada e Saída de dados binários,
independente da plataforma.
Começaremos escrevendo um arquivo de Planilha:
Visão do Código:
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("Cannot write file %1:n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_3);
out << quint32(MagicNumber);
QApplication::setOverrideCursor(Qt::WaitCursor);
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
QString str = formula(row, column);
if (!str.isEmpty())
out << quint16(row) << quint16(column) << str;
}
}
QApplication::restoreOverrideCursor();
return true;
}
A função WriteFile() é executada de MinWindow::saveFile() para escrever o
arquivo no disco. Retorna true se tudo der certo, e false em casos de erro.
Criamos um objeto QFile com o nome dado e chamamos open() para abrir o
arquivo para escrita. Também criamos um objeto do tipo QDataStream que opera no
arquivo QFile e o usa para processar os dados.
Antes de escrever os dados, mudamos o cursor da aplicação para o cursos de
espera padrão ( ampulheta, geralmente) e o restauramos para o desenho normal
assim que toda a data for escrita. No final da função, o arquivo é fechado
automaticamente pelo destrutor de QFile.
QDataStream suporta os tipos básicos de C++ assim como muitos dos tipos em Qt.
A sintaxe é modelada depois das classes padrão C++ <iostream>. Por exemplo,
Out << x << y << z;
Escreve as variáveis x,y e z em um fluxo, e
In >> x >> y >> z;
As lê do fluxo. Devido ao fato dos tipos primitivos inteiros de C++ terem tamanhos
diferentes em plataformas diferentes, é mais seguro converter estes valores em
valores do tipo qint8, qint16, qint32,qint32, qint64, e qint64, que são
certamente estarão em um tamanho que eles propõem( em bits).
A formato do arquivo da aplicação da Planilha é bem simples. Uma planilha inicia
com um número de 32 bits o qual identifica o formato do arquivo(MagicNumber,
definido como 0x7F51C883 em spreadsheet.h, um número aleatório arbitrário).
Depois vem uma série de blocos, cada um contendo coluna, linha e fórmula de cada
célula. Para economizar espaço, não escrevemos em células vazias. O formato é
mostrado na figura 4.3:
Figura 4.3: O formato do arquivo Planilha
A representação binária precisa dos tipos de dados é determinada por
QDataStream.Por exemplo, um quint16 é armazenado como dois bytes com o mais
significativo à esquerda, e um QString como o tamanho da string seguido de
caracteres Unicode.
A representação binária dos tipos Qt tem evoluído bastante desde Qt 1.0. Tende-se
a continuar evoluindo em releases futuros para manter sincronia com a evolução
dos tipos existentes e permitir entrada de novos tipos Qt. Por padrão, QDataStream
usa a mais recente versão do formato Binário (versão 9 em Qt 4.3), mas pode ser
ajustado para ler versões mais antigas. Para evitar problemas de compatibilidade
caso a aplicação seja recompilada mais tarde usando um release mais atual,
dizemos QDataStream para utilizar versão 9 independentemente da versão de Qt
que estivermos compilando. (QDatasStream::Qt_4_3 é uma constante que resulta
em 9.)
QDataStream é muito versátil. Pode ser usada em um arquivo QFile, e também em
um QBuffer, um QProcess, um QTcpSocket, um QUdpSocket, ou um QSslSocket. Qt
também oferece uma classe QTextStream que pode ser usada ao invés de
QDataStream para ler/escrever arquivos de texto. O capítulo 12 explica essas
classes mais detalhadamente, e também descreve várias abordagens para controlar
versões diferentes de QDataStream.
Código:
bool Spreadsheet::readFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("Cannot read file %1:n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream in(&file);
in.setVersion(QDataStream::Qt_4_3);
quint32 magic;
in >> magic;
if (magic != MagicNumber) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("The file is not a Spreadsheet
file."));
return false;
}
clear();
quint16 row;
quint16 column;
QString str;
QApplication::setOverrideCursor(Qt::WaitCursor);
while (!in.atEnd()) {
in >> row >> column >> str;
setFormula(row, column, str);
}
QApplication::restoreOverrideCursor();
return true;
}
A função readFile()é muito similar à writeFile(. Usamos QFile para ler no
arquivo, mas dessa vez usando a flag QIODevice::ReadOnly ao invés de
QIODevice::WriteOnly. Depois setamos a versão de QDataStream para 9. O
formato para leitura deve ser sempre o mesmo para escrita.
Se o arquivo tem o número mágico correto no início, chamamos
clear() para limpar todas as células da
planilha, e lemos nos dados da célula. Já que o arquivo contém
apenas os dados de células não-vazias, e é
muito improvável que todas as células na planilha sejam definidas,
temos de assegurar que todas as células sejam removidas antes
da leitura.
Implementando o Menu Editar
Estamos prontos para implementar os slots que correspondem ao menu Edit da
aplicação. O menu é mostrado na Figura 4.4.
Figura 4.4. O Menu Edit da aplicação da Planilha
void Spreadsheet::cut()
{
copy();
del();
}
O slot cut() corresponde a Edit|Cut. A implementação é simples já que Cut é o
mesmo de Copy, seguido por Delete.
void Spreadsheet::copy()
{
QTableWidgetSelectionRange range = selectedRange();
QString str;
for (int i = 0; i < range.rowCount(); ++i) {
if (i > 0)
str += "n";
for (int j = 0; j < range.columnCount(); ++j) {
if (j > 0)
str += "t";
str += formula(range.topRow()+i,range.leftColumn() +j);
}
}
QApplication::clipboard()->setText(str);
}
O slot copy() corresponde a Edit|Copy. Ele age sobre a área selecionada ( que se
torna a célula atual, caso nada tenha sido selecionado). Cada célula selecionada é
adicionada a uma QString, com linhas separadas por caracteres de nova linha, e
colunas separadas por caracteres de parágrafo. Isto é ilustrado na Figure 4.5.
Figura 4.5. Copiando uma seleção para Área de Transferência
A área de transferência do sistema está disponível em Qt através da função
estátitca QApplication::clipboard(). Através da chamada de
QClipboard::setText(), deixamos o texto disponível na área de transferência, não
só para esta aplicação mas também para as demais aplicações que suportem texto
simples. Nosso formato, que utiliza caracteres de parágrafo e nova linha como
separadores, é identificado por uma série de aplicações, incluindo Microsoft Excel.
A função QTableWidget::selectedRanges() retorna uma lista de comprimentos de
seleção. Sabemos que não poderá existir mais de um, pois setamos o modo de
seleção para QAbstractItemView::ContiguousSelection no construtor. Para nossa
conveniência, definimos uma função selectedRange() que retorna o comprimento
da seleção:
QTableWidgetSelectionRange Spreadsheet::selectedRange() const
{
QList<QTableWidgetSelectionRange> ranges = selectedRanges();
if (ranges.isEmpty())
return QTableWidgetSelectionRange();
return ranges.first();
}
Se há uma seleção de tudo, basta devolver a primeira (e
única). Deve sempre haver uma seleção já que
o modo ContiguousSelection trata a célula atual como sendo
selecionada. Mas para proteger contra a possibilidade
de um bug no nosso programa que não faz nenhuma célula ser a
atual, nós tratamos este caso.
Código:
void Spreadsheet::paste()
{
QTableWidgetSelectionRange range = selectedRange();
QString str = QApplication::clipboard()->text();
QStringList rows = str.split('n');
int numRows = rows.count();
int numColumns = rows.first().count('t') + 1;
if (range.rowCount() * range.columnCount() != 1
&& (range.rowCount() != numRows
|| range.columnCount() != numColumns)) {
QMessageBox::information(this, tr("Spreadsheet"),
tr("The information cannot be pasted because the copy "
"and paste areas aren't the same size."));
return;
}
for (int i = 0; i < numRows; ++i) {
QStringList columns = rows[i].split('t');
for (int j = 0; j < numColumns; ++j) {
int row = range.topRow() + i;
int column = range.leftColumn() + j;
if (row < RowCount && column < ColumnCount)
setFormula(row, column, columns[j]);
}
}
somethingChanged();
}
O slot paste() corresponde a Edit|Paste. Buscamos o texto na Área de Transferência
e chamamos a função estática QString::split() para quebrar a string em um
QStringList. Cada linha se torna uma string na lista.
Depois, determinamos as dimensões da área de cópia. O número de linhas é o
número de strings na QStringList; o número de colunas é o número de caracteres
Parágrafo na primeira linha, mais 1. Se apenas uma célula for selecionada, usamos
essa célula como o canto esquerdo superior da área de cola; caso haja mais de uma
célula, usamos a seleção como área de cola.
Para realizar a ação de colar, permutamos as linhas e as dividimos em células
usando QString::split() novamente, mas dessa vez usando parágrafo como
separador. A Figura 4.6 ilustra as etapas.
Figura 4.6. Colando texto da área de transferência na Planilha.
void Spreadsheet::del()
{
QList<QTableWidgetItem *> items = selectedItems();
if (!items.isEmpty()) {
foreach (QTableWidgetItem *item, items)
delete item;
somethingChanged();
}
}
O slot del() corresponde a Edit|Delete. Se houver itens selecionados, a função os
apaga e faz uma chamada para somethingChanged(). É suficiente usar delete em
cada objeto Cell na seleção para limpar células. QTableWidget notifica quando
seus itens de QTableWidgetItem são deletados e automaticamente se re-pinta caso
algum dos itens esteja disponível. Se chamarmos cell() com a localização de uma
célula deletada, vai retornar um ponteiro para null.
void Spreadsheet::selectCurrentRow()
{
selectRow(currentRow());
}
void Spreadsheet::selectCurrentColumn()
{
selectColumn(currentColumn());
}
As funções selectCurrentRow() e selectCurrentColumn() corresponde às opções
Edit|Select|Row e Edit|Select|Column no menu. As implementações são baseadas
nas funções de QTableWidget, selectRow() e selectColumn(). Não precisamos
implementar a funcionalidade por trás de Edit|Select|All, já que é fornecida pela
função herdada de QTableWidget, QAbstractItemView::selectAll().
void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs)
{
int row = currentRow();
int column = currentColumn() + 1;
while (row < RowCount) {
while (column < ColumnCount) {
if (text(row, column).contains(str, cs)) {
clearSelection();
setCurrentCell(row, column);
activateWindow();
return;
}
++column;
}
column = 0;
++row;
}
QApplication::beep();
}
O slot findText() varre através das células iniciando da célula à direita do cursor,
se movendo pela direita até a última coluna, e depois continua da primeira coluna
da linha abaixo, e assim em diante até o texto ser encontrado ou até atingir a
última célula. Por exemplo, se a célula atual for C24, pesquisamos D24, E24,..., Z24,
A25, B25,..., Z25, até Z999.
Se encontrarmos o texto correspondente, limpamos a seleção atual, movemos o
cursor de células até a célula contendo o texto encontrado, e ativamos a janela que
contém Spreadsheet. Se não for encontrado o texto, fazemos a aplicação emitir um
alerta, avisando que a busca encerrou sem sucesso.
void Spreadsheet::findPrevious(const QString &str,
Qt::CaseSensitivity cs)
{
int row = currentRow();
int column = currentColumn() - 1;
while (row >= 0) {
while (column >= 0) {
if (text(row, column).contains(str, cs)) {
clearSelection();
setCurrentCell(row, column);
activateWindow();
return;
}
--column;
}
column = ColumnCount - 1;
--row;
}
QApplication::beep();
}
O slot findPrevious() é similar ao slot findText(), exceto que ele pesquisa de
frente para trás, e pára na célula A1.
Implementando os Outros Menus
Vamos agora implementar os slots para os menus Tools e Options. Esses menus são
exibidos na Figura 4.7.
Figura 4.7. Os menus Tools e Options da aplicação da Planilha.
void Spreadsheet::recalculate()
{
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
if (cell(row, column))
cell(row, column)->setDirty();
}
}
viewport()->update();
}
O slot recalculate() corresponde a Tools|Recalculate. Também é chamado
automaticamente por Spreadsheet quando necessário.
Vasculhamos as células e chamamos setDirty() em cada célula para marcar as que
serão recalculadas. Da próxima vez que QTableWidget chamar text() em uma Cell
para obter o valor para mostrar na planilha, o valor será recalculado.
Depois chamamos update() no viewport para editar a planilha inteira. O código de
edição em QTableWidget chama text() em cada célula visível para obter o valor para
exibir. Já que fizemos chamada a setDirty() em cada célula, as chamadas a text()
usarão um valor recém-calculado. O cálculo pode requerer células não-visíveis para
serem recalculadas, cascateando o cálculo até que cada célula que precisa ser
recalculada para mostrar o valor correto seja recalculada. O cálculo é feito pela
classe Cell.
void Spreadsheet::setAutoRecalculate(bool recalc)
{
autoRecalc = recalc;
if (autoRecalc)
recalculate();
}
O slot setAutoRecalculate() corresponde a Options|Auto-Recalculate. Se estiver
ativado, faz com que seja recalculada a planilha inteira imediatamente para
assegurar que está atualizada; depois, recalculate() é chamado automaticamente
de somethingChanged().
Não precisamos implementar nada para Options|Show Grid já que QTableWidget
possui um slot setShowGrid(), que é herdado de QTableView. Resta apenas
Spreadsheet::sort(), vindo de MainWindow::sort():
void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
QList<QStringList> rows;
QTableWidgetSelectionRange range = selectedRange();
int i;
for (i = 0; i < range.rowCount(); ++i) {
QStringList row;
for (int j = 0; j < range.columnCount(); ++j)
row.append(formula(range.topRow() + i,
range.leftColumn() + j));
rows.append(row);
}
qStableSort(rows.begin(), rows.end(), compare);
for (i = 0; i < range.rowCount(); ++i) {
for (int j = 0; j < range.columnCount(); ++j)
setFormula(range.topRow() + i, range.leftColumn() + j,
rows[i][j]);
}
clearSelection();
somethingChanged();
}
Ordenação opera na seleção feita e reordena as linhas de acordo com as chaves de
ordenação e ordens de ordenação, armazenados no objeto compare.
Representamos cada linha de dados com uma QStringList e armazenamos a seleção
como uma lista de linhas. Usamos o algoritmo qStableSort(), advindo do Qt, e para
simplificar sorteamos por fórmula, ao invés de valor. O processo é ilustrado nas
Figuras 4.8 e 4.9. Cobrimos os algoritmos-padrão e estruturas de dados para Qt no
Capítulo 11.
Figura 4.8. Armazenando a seleção como uma lista de linhas
Figura 4.9. Colocando dados de volta na tabela, após ordenação
A função qStableSort() aceita um iterador inicial, um iterador final, e uma função de
comparação. A função de comparação é uma função que leva dois
argumentos(duas QStringsList) e retorna true caso o primeiro argumento é “menor
que” o segundo argumento, e false caso contrário. O objeto compare que passamos
como função comparadora não é realmente uma função, mas pode ser usado como
uma, como veremos resumidamente.
Depois de realizar QStableSOrt(), movemos os dados de volta à tabela, limpamos a
seleção, e chamamos somethingChanged().
Em Spreadsheet.h, c classe SpdeadsheetCompare foi definida de seguinte forma:
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
};
A classe SpreadSheetCompare é especial porque implementa o operador (). Isto nos
permite utilizar a classe como se fosse uma função. Tais classes são chamadas
function objects, ou functors. Oara entender como functors atuam, iniciemos com
um exemplo simples:
class Square
{
public:
int operator()(int x) const { return x * x; }
}
A ckasse Square fornece uma função, operator() (int), que retorna o quadrado de
seu parâmetro. Nomeando a função como operator() (int), ao invés de compute(int),
por exemplo, ganha-se a capcidade de se usar um objeto do tipo Square como se
ele fosse uma função:
Square square;
int y = square(5);
// y equals 25
Agora vejamos um exemplo envolvendo SpreadsheetCompare:
QStringList row1, row2;
SpreadsheetCompare compare;
if (compare(row1, row2)) {
// row1 is less than row2
}
O objeto compare pode ser usado bem como se fosse uma simples função
compare(). Adicionalmente, sua implementação pode acessar todas as chaves e
ordens de combinações, que são armazenadas como variáveis membros.
Uma alternativa para este esquema seria armazenar chaves e ordens em variáveis
globais, e usar um função simples compare(). Entretanto, não é muito elegante se
comunicar entre variáveis globais, além do risco de bugs. Functors são uma forma
mais forte para criar uma interface com as funções do tipo template, como
qStableSort();
Aqui temos uma implementação da função que é usada para comparar duas linhas
de planilhas:
bool SpreadsheetCompare::operator()(const QStringList &row1,
const QStringList &row2) const
{
for (int i = 0; i < KeyCount; ++i) {
int column = keys[i];
if (column != -1) {
if (row1[column] != row2[column]) {
if (ascending[i]) {
return row1[column] < row2[column];
} else {
return row1[column] > row2[column];
}
}
}
}
return false;
}
O operador retorna true caso a primeira linha seja menor do que a segunda linha;
do contrário, retorna false. A função qStableSort() usa o resultado desta função para
realizar a ordenação.
As Keys dos objetos de SpreadsheetCompare as arrays ascending são povoadas na
função MainWindow::sort() ( mostrada no capítulo 2). Cada chave possui um
endereço de coluna, ou um valor -1 ( “Nada”).
Comparamos as entradas da célula correspondente nas duas linhas para cada
chave em ordem. Assim que encontramos uma diferença, retornamos um valor
apropriado true ou false. Se todas as comparações acabam por ser
iguais, voltamos o valor false. A função qStableSort ()usa a ordem
antes da classificação para resolver situações de empate; se row1
precedeu row2 originalmente e nem se compara como "inferior"
ao outro, row1 ainda precede row2 no resultado. Isto distingue
qStableSort () do seu primo instável qsort ().
Completamos agora a classe Spreadsheet. Na próxima sessão,
vamos revisar a classe Cell. Essa classe é usada para guardar
fórmulas de células e fornece uma reimplementação da função
QTableWidgetItem::data(), que é chamada indiretamente por
Spreadshet, através da função QTableWidgetIetm::text(), para
exibir o resultado do cálculo em uma célula.
A Subclasse QTableWidgetItem
A classe Cell é derivada de QTableWidgetItem. A classe é designada para trabalhar
bem como Spreadsheet, mas não possui dependências específicas naquela classe e,
em teoria, pode ser usada em qualquer QTableWidget. Aqui está o arquivo
cabeçalho:
Código:
#ifndef CELL_H
#define CELL_H
#include <QTableWidgetItem>
class Cell : public QTableWidgetItem
{
public:
Cell();
QTableWidgetItem *clone() const;
void setData(int role, const QVariant &value);
QVariant data(int role) const;
void setFormula(const QString &formula);
QString formula() const;
void setDirty();
private:
QVariant value() const;
QVariant evalExpression(const QString &str, int &pos) const;
QVariant evalTerm(const QString &str, int &pos) const;
QVariant evalFactor(const QString &str, int &pos) const;
mutable QVariant cachedValue;
mutable bool cacheIsDirty;
};
#endif
A Classe cell estende QTableWidgetItem através da adição de duas variáveis
provadas:
• cachedValue armazena em cachê o valor da célula como um QVariant.
• cachedIsDirty, valor booleano que é true caso o valor guardado em cachê
não está atualizado.
Usamos QVariant porque algumas células apresentam valores do tipo Double,
enquanto outras possuem valor do tipo QString.
As variáveis cachedValue e cacheIsDirty são declaradas com a palavra-chave
mutable do C++. Isto permite-nos modificar essas variáveis em funções const.
Alternativamente, recalcularíamos o valor cada vez que text() é chamado, um
procedimento um tanto ineficiente.
Note que não há, na definição da classe, um macro Q_OBJECT. Cell é uma classe
C++ simples, sem sinais ou slots. Na verdade, já que QTableWidget não é derivada
de QObject, nunca poderemos ter sinais nem slots na classe Cell enquanto ela
estiver viva. Classes de itens do Qt não são derivadas de QObject afim de se
manter seu custo o menor possível. Se forem necessários sinais e slots, podem ser
implementados no widget que contiver os itens ou, excepcionalmente, usando
herança múltipla com QObject.
Aqui está o início de cell.cpp:
#include <QtGui>
#include “cell.h”
Cell::cell()
{
setDirty();
}
No construtor, apenas precisamos marcar a cachê como dirty. Não há necessidade
de passar uma superclasse; quando a célula é inserida em um QTableWidget com
setItem(), o QTableWidget vai tomar posse dele automaticamente.
Toda QTableWidgetItem pode guardar algum dados, que podem ser no máximo um
QVariant para cada “papel” que cada dado possui. Os papéis mais assumidos são
Qt::EditRole e Qt::DisplayRole. Papel de edição é usado para dados que estão
para serem editados, e o papel de exibição, para dados que estão para serem
exibidos. Freqüentemente os dados para ambos são os mesmos, mas na classe
Cell o papel de edição corresponde às fórmulas das células e a tarefa de display
corresponde ao valor da célula (o resultado da avaliação da fórmula).
QTableWidgetItem *Cell::clone() const
{
return new Cell(*this);
}
A função clone()é chamada por QTableWidget quando necessita criar uma nova
célula – por exemplo, quando o usuário começa a digitar em uma célula vazia que
nunca foi usada antes. A instância passada para
QTableWIdget::setItemPrototype() é o item que é clonado. Já que uma cópia do
melhor membro é suficiente para Cell, estamos confiando no construtor de cópia
padrão automaticamente criado pelo C++ para criar novas instancias de Cell na
função clone().
void Cell::setFormula(const QString &formula)
{
setData(Qt::EditRole, formula);
}
A função setFormula() determina a fórmula da célula. É simplesmente uma função
de conveniência que chama setData() com a tarefa de editar. Ela vem de
Spreadsheet::setFormula().
QString Cell::formula() const
{
return data(Qt::EditRole).toString();
}
A função formula() é chamada de Spreadsheet::formula(). Assim como setFormula(),
é uma função de conveniência, desta vez recuperando a data do item em Edit Role.
void Cell::setData(int role, const QVariant &value)
{
QTableWidgetItem::setData(role, value);
if (role == Qt::EditRole)
setDirty();
}
Se tivermos uma nova fórmula, marcamos true em cacheIsDirty para assegurar
que a célula é recalculada da próxima vez que text() for chamada.
Não existe uma função text() definida em Cell, apesar de chamarmos texxt()
em instâncias de Cell em Spreadsheet::text(). A função text() é uma
conveniência dada por QTableWidgetItem; é o equivalente a chamar
data(Qt::DisplaRole).toString().
void Cell::setDirty()
{
cacheIsDirty = true;
}
A função setDirty() é chamada para forçar um re-cálculo do valor da célula. Ele
simplesmente habilita true em cacheIsDirty, o que significa que cachedValue não
estará mais atualizado. O re-cálculo não é realizado enquanto não for necessário.
QVariant Cell::data(int role) const
{
if (role == Qt::DisplayRole) {
if (value().isValid()) {
return value().toString();
} else {
return "####";
}
} else if (role == Qt::TextAlignmentRole) {
if (value().type() == QVariant::String) {
return int(Qt::AlignLeft | Qt::AlignVCenter);
} else {
return int(Qt::AlignRight | Qt::AlignVCenter);
}
} else {
return QTableWidgetItem::data(role);
}
}
A função data() é re-implementada de QTableWidgetItem. Ela retorna o texto que
deve ser mostrado na planilha se chamado com Qt::DissplayRole, e a fórmula se
chamado com Qt::EditRole. Retorna um alinhamento apropriado se chamado com
Qt::TextAlignmentRole. No caso de DisplayRole, ela depende de value() para
computar o valor da célula. Caso o valor seja inválido (decorrente de fórmula
incorreta), retornamos “####”.
A função Cell::value() usada em data() retorna um QVariant. Um QVariant
pode armazenar diversos valores de diferentes tipos, como Double e QString, e
fornece funções para conversão da variante em outros tipos. Por exemplo, chamar
toString() em uma variante que guarda um valor double produz uma
representação em string do valor em double. Um QVariant construído usando um
construtor default é um variante “inválido”.
Código:
const QVariant Invalid;
QVariant Cell::value() const
{
if (cacheIsDirty) {
cacheIsDirty = false;
QString formulaStr = formula();
if (formulaStr.startsWith(''')) {
cachedValue = formulaStr.mid(1);
} else if (formulaStr.startsWith('=')) {
cachedValue = Invalid;
QString expr = formulaStr.mid(1);
expr.replace(" ", "");
expr.append(QChar::Null);
int pos = 0;
cachedValue = evalExpression(expr, pos);
if (expr[pos] != QChar::Null)
cachedValue = Invalid;
} else {
bool ok;
double d = formulaStr.toDouble(&ok);
if (ok) {
cachedValue = d;
} else {
cachedValue = formulaStr;
}
}
Return cachedValue;
}
A função privada value() retorna o valor da célula. Caso cacheIsDirty seja true,
precisamos recalcular o valor.
Se a fórmula começa com um apóstrofo( e.g., “’12345”), tomamos a string a partir
da posição 1 e removemos quaisquer espaços que ela possa conter. Depois,
chamamos evalExpression() para computar o valor da expressão. O argumento
pos é passado por referência; indica a posição do caractere onde a análise deve
começar. Após a chamada para evalExpression(), o caractere para posição pos
deve ser o caractere QChar::Null que anexamos, caso tenha sido analisado com
sucesso. Caso a análise tenha falhado antes do fim, ajustamos cacheValue para
Invalid.
Se a fórmula não começar com aspa simples ou um sinal de igual, devemos tentar
convertê-la em um valor ponto-flutuante, usando toDouble(). Caso a conversão
funcione, ajustamos cachedValue para ser o valor resultante; se não funcionar,
ajustamos cachedValue para ser a string da fórmula. Por exemplo, uma fórmula de
“1.50” faz com que toDouble() marque ok em true e retorne 1.5, enquanto que a
fórmula de “World Population” faz com que toDouble() marque ok em false, e
retorne 0.0.
Ao dar um ponteiro para bool a toDouble(), se torna possível distinguir entre a
conversão de uma string que representa o valor numérico 0.0 e um erro de
conversão ( onde 0.0 também é retornado, porém o valor bool está marcado como
false). Ás vezes o fato de se retornar um valor zero em erro de conversão é
exatamente o que precisamos, já que no caso não nos preocupamos em passar um
ponteiro para bool. Por razões de portabilidade e performance, Qt nunca usa
exceções do C++ para reportar falha. Isso não o previne de usá-las em programas
Qt caso seu compilador as suporte.
A função value() é declarada constante. Tivemos de declarar cachedValue e
cacheIsValid como variáveis mutáveis para que o compilador nos permita
modificá-las em funções constantes. Pode ser tentador fazer value() uma função
não-constante e remover as keywords mutable, mas isso não compilaria, pois
chamamos value)_ de data(), uma função constante.
Terminamos assim a aplicação Spreadsheet, incluindo a análise de fórmulas. O
restante desta seção cobre evalExpression() e outras duas funções de caráter de
ajuda, evalTerm() e evalFactor(). O código é um tanto complicado, mas consta
aqui para tornar a aplicação completa. Devido ao fato do código não estar
relacionado à programação GUI, você pode seguramente pular esta etapa e
continuar a leitura no Capítulo 5.
A função evalExpression() retorna o valor da expressão de uma planilha. Uma
expressão é definida como um ou mais termos separados por ‘+’ ou ‘-‘. Os termos
são definidos como um ou mais fatores, separados por operadores ‘*’ ou ‘/’. Pela
quebra de expressões em termos, e, consequentemente, em fatores, asseguramos
que os operadores são aplicados com a precedência correta.
Por exemplo, “2*C5+D6” é um expressão cujo primeiro termo é “2*C5”, e o
segundo termo é “D6”. O Termo “2*C5” possui “2’ como primeiro fator, e “C5” como
segundo fator, e o termo “D6” consiste em um fator singular. Um fator pode ser um
número ( “2”), uma localização de célula (“C5”), ou uma expressão em parênteses,
opcionalmente precedida por um unário “-“.
A sintaxe de expressões da planilha é definida na Figura 4.10. Para cada símbolo na
gramática (Expressão, Termo e Fator), existe uma função-membro correspondente
que o analisa e cuja estrutura segue fielmente a gramática. Analisadores escritos
dessa forma são chamados analisadores recursivos descententes.
Figura 4.10. Diagrama de Sintaxe para expressões da planilha
Vamos começar com evalExpression(), a função que analisa uma Expression:
Código:
QVariant Cell::evalExpression(const QString &str, int &pos) const
{
QVariant result = evalTerm(str, pos);
while (str[pos] != QChar::Null) {
QChar op = str[pos];
if (op != '+' && op != '-')
return result;
++pos;
QVariant term = evalTerm(str, pos);
if (result.type() == QVariant::Double
&& term.type() == QVariant::Double) {
if (op == '+') {
result = result.toDouble() + term.toDouble();
} else {
result = result.toDouble() - term.toDouble();
}
} else {
result = Invalid;
}
}
return result;
}
Primeiro, chamamos evalTerm() para obter o valor do primeiro termo. Se o caractere
seguinte for ‘+’ ou ‘-‘, continuamos chamano evalTerm() mais uma vez; do
contrário, a expressão consiste em um único termo, e retornamos seu valor como o
valor da expressão inteira. Após obtermos o valor dos dois primeiros termos,
computamos o resultado da operação, de acordo com o operador. Caso ambos os
termos tenham sido levados a um tipo double, computamos o resultado como um
double; do contrário, setamos o resultado como Invalid.
Continuamos dessa forma até que não haja mais termos. Isto funciona
corretamente pois adição e subtração são associativas à esquerda; ou seja, “1-2-3”
significa “(1-2)-3”, e não “1-(2-3)”.
Código:
QVariant Cell::evalTerm(const QString &str, int &pos) const
{
QVariant result = evalFactor(str, pos);
while (str[pos] != QChar::Null) {
QChar op = str[pos];
if (op != '*' && op != '/')
return result;
++pos;
QVariant factor = evalFactor(str, pos);
if (result.type() == QVariant::Double
&& factor.type() == QVariant::Double) {
if (op == '*') {
result = result.toDouble() * factor.toDouble();
} else {
if (factor.toDouble() == 0.0) {
result = Invalid;
} else {
result = result.toDouble() / factor.toDouble();
}
}
} else {
result = Invalid;
}
}
return result;
}
A função evalTerm() é muito similar à evalExpression(), exceto pelo fato de que
trabalha com multiplicação e divisão. A única sutileza em evalTerm() é que devemos
tratar a divisão por zero, já que é um erro em alguns processadores. Já que é
desaconselhável testar valores em ponto flutuante para igualdade, devido a erros
de arredondamento, é mais seguro testar igualdade em relação a 0.0 para se evitar
divisão por zero.
Código:
QVariant Cell::evalFactor(const QString &str, int &pos) const
{
QVariant result;
bool negative = false;
if (str[pos] == '-') {
negative = true;
++pos;
}
if (str[pos] == '(') {
++pos;
result = evalExpression(str, pos);
if (str[pos] != ')')
result = Invalid;
++pos;
} else {
QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");
QString token;
while (str[pos].isLetterOrNumber() || str[pos] == '.') {
token += str[pos];
++pos;
}
if (regExp.exactMatch(token)) {
int column = token[0].toUpper().unicode() - 'A';
int row = token.mid(1).toInt() - 1;
Cell *c = static_cast<Cell *>(
tableWidget()->item(row, column));
if (c) {
result = c->value();
} else {
result = 0.0;
}
} else {
bool ok;
result = token.toDouble(&ok);
if (!ok)
result = Invalid;
}
}
if (negative) {
if (result.type() == QVariant::Double) {
result = -result.toDouble();
} else {
result = Invalid;
}
}
return result;
}
A função evalFactor() é um pouco mais complicada do que evalExpression() e
evalTerm().Começamos constatando onde o fator é negado. Então, vemos se este
trecho começa com um parênteses abertos. Se abrir, avaliamos o conteúdo de
parênteses e como uma expressão através da chamada a evalExpression().
Quando analisa uma expressão em parênteses, evalExpression() faz uma
chamada a evalTerm(), que chama evalFactor(), que por sua vez chama
evalExpression() novamente. É aqui que a recursividade ocorre no analisador.
Caso o fator não seja uma expressão aninhada, extraímos o próximo token, que
deve ser uma localização de célula ou um número. Caso o token case com
QRegExp, passamos a tratá-lo como um referência a célula e chamamos value() na
célula que possui tal endereço. A célula pode estar em qualquer lugar na planilha, e
pode ter dependências em outras células. As dependências não são um problema;
elas vão simplesmente disparar mais chamadas a value() e ( para células “sujas”),
mais análise até que todas os valores de células dependetes sejam calculados. Caso
o token não seja uma localização de célula, o tratamos como um número.
O que acontece se a célula A1 contem a fórmula “=A1”? Ou se a célula A1 contem
“=A2” e a célula A2 contem “=A1”?
Apesar de não termos escrito nenhum código especial para detectar dependências
cíclicas, o analisador controla estes casos prontamente, retornando um inválido
QVariant. Isto funciona porque ajustamos cacheisDirty para false e cachedValue
para Invalid em value() antes de chamarmos evalExpression(). Caso
evalExpression() chame recursivamente value() na mesma célula, ela retorna
Invalid imediatamente, e a expressão por inteiro se torna Invalid.
Completamos, enfim, o analisador de fórmula. Seria justo estender isto para
funções predefinidas na planilha, como “sum()” e “avg()”, através da extensão da
definição gramatical de Fator. Outra extensão simples seria a implementação do
operador de “+” com strings como operandos ( ou seja, uma concatenação); isto
não requer mudanças na gramática.

Mais conteúdo relacionado

Destaque

Ventajas juegos cooperativos
Ventajas juegos cooperativosVentajas juegos cooperativos
Ventajas juegos cooperativos
gradoprimaria
 
A lentidão do seu sistema tem solução
A lentidão do seu sistema tem soluçãoA lentidão do seu sistema tem solução
A lentidão do seu sistema tem solução
Luis Brito
 
Encuesta CEIS Consultora CABA Julio 2013
Encuesta CEIS Consultora CABA Julio 2013Encuesta CEIS Consultora CABA Julio 2013
Encuesta CEIS Consultora CABA Julio 2013
Eduardo Nelson German
 
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
Eduardo Nelson German
 
Codigo especifico de comunicación
Codigo especifico de comunicaciónCodigo especifico de comunicación
Codigo especifico de comunicación
Adriana Pilar
 
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa martaTabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
MONTERO ESCUELA DEPORTIVA
 

Destaque (20)

Ventajas juegos cooperativos
Ventajas juegos cooperativosVentajas juegos cooperativos
Ventajas juegos cooperativos
 
Semana do meio ambiente
Semana do meio ambienteSemana do meio ambiente
Semana do meio ambiente
 
Migració de Windows a GNU Linux Ubuntu
Migració de Windows a GNU Linux UbuntuMigració de Windows a GNU Linux Ubuntu
Migració de Windows a GNU Linux Ubuntu
 
Presentació de Hiren's boot 15.2
Presentació de Hiren's boot 15.2Presentació de Hiren's boot 15.2
Presentació de Hiren's boot 15.2
 
El deporte
El deporteEl deporte
El deporte
 
Presentació de Google nexus 7
Presentació de Google nexus 7Presentació de Google nexus 7
Presentació de Google nexus 7
 
A lentidão do seu sistema tem solução
A lentidão do seu sistema tem soluçãoA lentidão do seu sistema tem solução
A lentidão do seu sistema tem solução
 
Encuesta CEIS Consultora CABA Julio 2013
Encuesta CEIS Consultora CABA Julio 2013Encuesta CEIS Consultora CABA Julio 2013
Encuesta CEIS Consultora CABA Julio 2013
 
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
Para Jefe de Gobierno: Michetti 18,7%, Lousteau 14%, R. Larreta 12%, Santilli...
 
O'meara Ferguson
O'meara FergusonO'meara Ferguson
O'meara Ferguson
 
Gei 06 (1)
Gei 06 (1)Gei 06 (1)
Gei 06 (1)
 
Codigo especifico de comunicación
Codigo especifico de comunicaciónCodigo especifico de comunicación
Codigo especifico de comunicación
 
Presentació de Calibre
Presentació de CalibrePresentació de Calibre
Presentació de Calibre
 
Presentació històrica de Visual.NET
Presentació històrica de Visual.NETPresentació històrica de Visual.NET
Presentació històrica de Visual.NET
 
Presentació de Magento
Presentació de MagentoPresentació de Magento
Presentació de Magento
 
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa martaTabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
Tabla goleadores a nov 20 2013 copa yogurcito-ciudadela _santa marta
 
Història de la impressió en 3D
Història de la impressió en 3DHistòria de la impressió en 3D
Història de la impressió en 3D
 
Sf vv2 es slideshare 17.6.14
Sf vv2 es slideshare 17.6.14Sf vv2 es slideshare 17.6.14
Sf vv2 es slideshare 17.6.14
 
Jóvenes rurales en red: Utilización de un grupo cerrado de FACEBOOK en el mar...
Jóvenes rurales en red: Utilización de un grupo cerrado de FACEBOOK en el mar...Jóvenes rurales en red: Utilización de un grupo cerrado de FACEBOOK en el mar...
Jóvenes rurales en red: Utilización de un grupo cerrado de FACEBOOK en el mar...
 
Tuik - Turkish Economic Indicators, may 2012 (TR-ENG)
Tuik - Turkish Economic Indicators, may 2012 (TR-ENG)Tuik - Turkish Economic Indicators, may 2012 (TR-ENG)
Tuik - Turkish Economic Indicators, may 2012 (TR-ENG)
 

Semelhante a Cap4

Desenvolvimento de Apps e Games para Android - Parte 6
Desenvolvimento de Apps e Games para Android - Parte 6Desenvolvimento de Apps e Games para Android - Parte 6
Desenvolvimento de Apps e Games para Android - Parte 6
Erisvaldo Junior
 
Curso avançado de c++ em portugues
Curso avançado de c++ em portuguesCurso avançado de c++ em portugues
Curso avançado de c++ em portugues
Laura
 
SelectionTracker Para Seleção de Itens no RecyclerView Android
SelectionTracker Para Seleção de Itens no RecyclerView AndroidSelectionTracker Para Seleção de Itens no RecyclerView Android
SelectionTracker Para Seleção de Itens no RecyclerView Android
Vinícius Thiengo
 
Apostila:Curso de java II
Apostila:Curso de java II  Apostila:Curso de java II
Apostila:Curso de java II
Verônica Veiga
 
Java interface gráfica layouts
Java   interface gráfica layoutsJava   interface gráfica layouts
Java interface gráfica layouts
Armando Daniel
 
Java - Gestão de componentes curso profissional
Java - Gestão de componentes curso profissionalJava - Gestão de componentes curso profissional
Java - Gestão de componentes curso profissional
a41172
 
Jason: Componentes personalizados
Jason: Componentes personalizados Jason: Componentes personalizados
Jason: Componentes personalizados
Nécio de Lima Veras
 

Semelhante a Cap4 (20)

Cap6
Cap6Cap6
Cap6
 
Gerenciadores de Layout
Gerenciadores de LayoutGerenciadores de Layout
Gerenciadores de Layout
 
Cap11
Cap11Cap11
Cap11
 
Usando a data grid wpf
Usando a data grid wpfUsando a data grid wpf
Usando a data grid wpf
 
Desenvolvimento de Apps e Games para Android - Parte 6
Desenvolvimento de Apps e Games para Android - Parte 6Desenvolvimento de Apps e Games para Android - Parte 6
Desenvolvimento de Apps e Games para Android - Parte 6
 
Cap12
Cap12Cap12
Cap12
 
THREADS EM JAVA: INTRODUÇÃO
THREADS EM JAVA: INTRODUÇÃOTHREADS EM JAVA: INTRODUÇÃO
THREADS EM JAVA: INTRODUÇÃO
 
Gwt
GwtGwt
Gwt
 
Interface Gráfica.ppt
Interface Gráfica.pptInterface Gráfica.ppt
Interface Gráfica.ppt
 
Curso avançado de c++ em portugues
Curso avançado de c++ em portuguesCurso avançado de c++ em portugues
Curso avançado de c++ em portugues
 
SelectionTracker Para Seleção de Itens no RecyclerView Android
SelectionTracker Para Seleção de Itens no RecyclerView AndroidSelectionTracker Para Seleção de Itens no RecyclerView Android
SelectionTracker Para Seleção de Itens no RecyclerView Android
 
Apostila:Curso de java II
Apostila:Curso de java II  Apostila:Curso de java II
Apostila:Curso de java II
 
Fazendo Injeção de dependência com Unity 1.2
Fazendo Injeção de dependência com Unity 1.2Fazendo Injeção de dependência com Unity 1.2
Fazendo Injeção de dependência com Unity 1.2
 
Java interface gráfica layouts
Java   interface gráfica layoutsJava   interface gráfica layouts
Java interface gráfica layouts
 
Testes em Aplicações Web com Cactus
Testes em Aplicações Web com CactusTestes em Aplicações Web com Cactus
Testes em Aplicações Web com Cactus
 
Python Interface Gráfica Tkinter
Python Interface Gráfica TkinterPython Interface Gráfica Tkinter
Python Interface Gráfica Tkinter
 
Java - Gestão de componentes curso profissional
Java - Gestão de componentes curso profissionalJava - Gestão de componentes curso profissional
Java - Gestão de componentes curso profissional
 
Mvc delphi
Mvc delphiMvc delphi
Mvc delphi
 
Java 17 Swing
Java 17 SwingJava 17 Swing
Java 17 Swing
 
Jason: Componentes personalizados
Jason: Componentes personalizados Jason: Componentes personalizados
Jason: Componentes personalizados
 

Cap4

  • 1. 4. Implementando Funcionalidade da Aplicação • A Widget Central • A Subclasse QTableWidget • Carregando e Salvando • Implementando o Menu Editar • Implementando os Outros Menus • A Subclasse QTableWidgetItem Nos dois últimos capítulos, explicamos como criar a Interface de Usuário para a aplicação com Planilha. Neste capítulo, vamos terminar o programa codificando suas funcionalidades básicas. Entre outras coisas, veremos como carregar e salvar arquivos, como armazenar dados na memória, como implementar operações em Área de Transferência, e como adicionar suporte às fórumas de planilhas para o QTableWidget. O Widget Central A área central de uma QMainWindow pode ser ocupado por qualquer tipo de Widget. Eis uma visão geral das possibilades: 1. Use uma Qt Widget Padrão. Uma widget padrão como a QTableWidget ou a QTextWidget podem ser usadas como Widgets centrais. Neste caso, a funcionalidade da aplicação, como carregar e salvar arquivos, devem ser implementados nos demais locais( em uma subclasse QMainWindow, por exemplo). 2. Use uma Qt Widget Personalizada.
  • 2. Aplicações especializadas freqüentemente necessitam mostrar dados em uma Widget Personalizada. Por exemplo, um programa editor de ícones teria um Widget IconEditor atuando como o Widget central. O capítulo 5 explica como escrever Widgets Personalizados em Qt. 3. Use uma Qt Widget limpa com um gerenciador de layouts. Ás vezes a área central da aplicação é ocupada por diversos widgets. Isso pode ser feito usando um QWidget como um pai de todos os demais widgets, e gerenciadores de layout para ajustar tamanho e posição das Widgets filhas. 4. Use um separador. Outra maneira de se usar diversas widgets juntas é utilizando um QSplitter. O QSplitter ordena suas widgets-filhas horizontal e verticalmente, com sontroladores de separação que dão um certo controle de tamanho ao usuário. Ordenadores podem conter todos os tipos de widgets, incluindo outros splitters. 5. Use uma área MDI. Se a aplicação usa MDI, a área central é ocupada por um QMdiArea Widget, e cada Janela MDI é uma instância daquela widget. Layouts, separadores e áreas MDI podem ser combinadas com Qt Widgets padrão ou com widgets customizados. O Capítulo 6 aborda essas classes com mais detalhes. Para a aplicação da Planilha, uma subclasse QTableWidget é usada como a widget central. A classe WTableWidget já possui a maior parte da capacidade da planilha de que precisamos, mas não suporta operações na área de transferência e não processa fórmulas de planilhas como “=A1+A2+A3”. Vamos precisar implementar essa funcionalidade que está faltando, na classe Spreadsheet.
  • 3. A Subclasse QTableWidget A Classe Planilha é derivada de QTableWidget, como mostra a figura 4.1. Uma QTableWidget é efetivamente uma grade que representa uma matriz esparsa bidimensional. Ela mostra todas as células para as quais o usuário se direcione, com suas respectivas dimensões. Quando o usuário entra algum texto em uma célula vazia, QTableWidget automaticamente cria um QTableWidgetItem para armazenar o próximo. Figura 4.1. Árvores de Herança para Planilha e Célula QTableWidget é derivado de QTableView, uma das classes de modelagem/visualização que analisaremos mais de perto no Capítulo 10. Outra tabela, que possui muito mais funcionalidades fora da caixa, é a QicsTable, disponível em Http://www.ics.com.com/. Vamos iniciar implementando Spreadsheet, começando pelo arquivo cabeçalho: #ifndef SPREADSHEET_H #define SPREADSHEET_H #include <QTableWidget> class Cell; class SpreadsheetCompare; O cabeçalho inicia com declarações para as classes Cell e SpreadsheetCompare Os atributos de uma célula de uma QTableWidget, como texto e alinhamento, são armazenados em uma QTableWidgetItem. Diferente de QTableWidget, a QTableWidgetItem não é uma classe widget; é puramente uma classe de dados. A
  • 4. classe Cell é derivada de QTableWidgetItem e serão explicados na última sessão deste capítulo. class Spreadsheet : public QTableWidget { Q_OBJECT public: Spreadsheet(QWidget *parent = 0); bool autoRecalculate() const { return autoRecalc; } QString currentLocation() const; QString currentFormula() const; QTableWidgetSelectionRange selectedRange() const; void clear(); bool readFile(const QString &fileName); bool writeFile(const QString &fileName); void sort(const SpreadsheetCompare &compare); A função autoRecalculate() implementada internamente já que ela apenas retorna se o auto-recálculo está “in force” ou não. No Capítulo 3, nós dependemos de algumas funções públicas na classe Spreadsheet quando implementamos MainWindow. Por exmplo, chamamos clear() de MainWindow::newFile() para resetar a planilha. Nós também usamos algumas funções herdadas de QTableWidget, como setCurrentCell() e setShowGrid(). public slots: void cut(); void copy(); void paste(); void del(); void selectCurrentRow(); void selectCurrentColumn(); void recalculate(); void setAutoRecalculate(bool recalc); void findNext(const QString &str, Qt::CaseSensitivity cs); void findPrevious(const QString &str, Qt::CaseSensitivity cs); signals: void modified(); Spreadsheet fornece vários slots que implementam ações dos menus Edit, Tools e Options, e também usa um sinal, modified(), para anunciar quando alguma mudança ocorrer. Private slots: Void somethingChanged(); Definimos um slot privado usado internamente pela classe Spreadsheet. private: enum { MagicNumber = 0x7F51C883, RowCount = 999, ColumnCount = 26 }; Cell *cell(int row, int column) const; QString text(int row, int column) const; QString formula(int row, int column) const; void setFormula(int row, int column, const QString &formula);
  • 5. bool autoRecalc; }; Na seção provada da classe, declaramos três constantes, quatro funções, e uma variável. class SpreadsheetCompare { public: bool operator()(const QStringList &row1, const QStringList &row2) const; enum { KeyCount = 3 }; int keys[KeyCount]; bool ascending[KeyCount]; }; #endif O cabeçalho termina com a definição da classe SpreadsheetCompare. Explicaremos isto quando revisarmos Spreadsheet::sort(). Vamos agora dar uma olhada na implementação: #include <QtGui> #include "cell.h" #include "spreadsheet.h" Spreadsheet::Spreadsheet(QWidget *parent) : QTableWidget(parent) { autoRecalc = true; setItemPrototype(new Cell); setSelectionMode(ContiguousSelection); connect(this, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(somethingChanged())); clear(); } Normalmente, quando o usuário entra com texto em uma célula vazia, o QTableWidget automaticamente criará um QTableWidgetItem para guardar o texto. Na nossa planilha, queremos que sejam criados itens de Cell no lugar. Isso é possível graças a chamada de setItemprototype() no construtor. Internamente, QTableWidget duplica o item passado como protótipo toda vez que um novo item é necessário. Ainda no construtor, setamos o modo de seleção para QAbstractItemView::ContiguousSelection para permitir uma seleção retangular única. Conectamos o sinal itemChanged() da widget da tabela para o slot privado somethingChanged(); isso assegura que quando o usuário editar uma célula, o slot somethingChanged() é chamado. Finalmente, chamamos clear() para redimensionar a tabela e ajustar os cabeçalhos das colunas. void Spreadsheet::clear() { setRowCount(0);
  • 6. setColumnCount(0); setRowCount(RowCount); setColumnCount(ColumnCount); for (int i = 0; i < ColumnCount; ++i) { QTableWidgetItem *item = new QTableWidgetItem; item->setText(QString(QChar('A' + i))); setHorizontalHeaderItem(i, item); } setCurrentCell(0, 0); } A função clear() é chamada do construtor Spreadsheet para inicializar a planilha. Ele também é chamado de MainWindow::newFile(). Poderíamos ter usado QTableWidget::clear() para limpar todos os itens e quaisquer seleções, mas isto deixaria os cabeçalhos em seus tamanhos atuais. Ao invés disso, redimensionamos a tabela para 0 x 0. Isso limpa a planilha inteira, incluindo os cabeçalhos. Depois redimensionamos a tabelas para ColumnCount x RowCount (26 x 999) e povoamos o cabeçalho horizontal com itens de QTableWidgetItem contendo os nomes “A”, “B”, ..., “Z”. Não precisamos ajustar os campos dos cabeçalhos verticais, já que estes possuem valores-padrão “1”, ”2”, ...,”999”. No final, movemos o cursor para a célula A1. Um QTableWidget é composto de diversos widgets-filhos. Tem um QHeaderView horizontal no topo, um QHeaderView vertical no lado esquerdo, e dois widgets WScrolBar. A área no meio é ocupada por um widget especial chamado de viewport, no qual QTableWidget desenha as células. Os diferentes widgets-filhos são acessíveis através das funções herdadas de QTableWidget e QAbstractScrollArea ( ver Figura 4.2). QAbstractScrollArea fornece um viewport rolável e duas barras de rolagem, que podem ser ligadas e desligadas. Veremos mais da subclasse QScrollArea no Capítulo 6. Figura 4.2: Widgets que constituiem QTableWidget Cell *Spreadsheet::cell(int row, int column) const { return static_cast<Cell *>(item(row, column)); }
  • 7. A função cell() returna o Objeto Cell para uma linha e coluna dadas. É quase o mesmo de QTableWidget::Item(). Exceto que retorna um ponteiro para Cell ao invés de um ponteiro para QTableWIdgetItem. QString Spreadsheet::text(int row, int column) const { Cell *c = cell(row, column); if (c) { return c->text(); } else { return ""; } } A função text(), privada, retorna o texto para uma dada célula. Caso cell() retorne um ponteiro null, a célula é vazia, então nós retornamos uma string vazia. QString Spreadsheet::formula(int row, int column) const { Cell *c = cell(row, column); if (c) { return c->formula(); } else { return ""; } } A função formula() retorna a fórmula da célula. Em muitos casos, a fórmula e o texto são os mesmos; por exemplo, a fórmula “Hello” equivale à string “Hello”, então se o usuário digita “Hello” em uma célula e aperta Enter, a célula mostra o texto “Hello”. Há algumas exceções, porém: • Caso a fórmula seja um número, é interpretada como tal. Por exemplo, a fórmula “1.50” equivale ao valor do tipo Double 1.5, que é passado como um valor alinhado à direita “1.5” na planilha. • Caso a fórmula inicie com aspas simples, o restante da fórmula é interpretado como texto. Por exemplo, a fórmula “’12345’” equivale à string “12345”. Armazenando Dados como Itens Na aplicação da Planilha, cada célula não-vazia é armazenada em memória como um Objeto QListWidgetItem individual. Armazenar dados como itens é uma abordagem que é usado inclusive por QListWidget e TreeWidget, o qual opera em elementos de QListWidgetItem e QtreeWidgetItem. Classes de itens do Qt possuem uma função a mais, armazenando mais informações. Por exemplo, um QTableWidgetItem já armazena alguns atributos, incluindo string, cor de fonte, ícone, e um ponteiro para QTableWIdget. Os itens podem também armazenar dados (QVariantS), incluindo tipos personalizados registrados, e através da herança da classe deste item, podem-se fornecer funcionalidades adicionais. Kits de ferramentas mais antigos possuem um ponteiro do tipo void em suas
  • 8. classes de itens para armazenar dados adicionais. No Qt, a ação mais natural é usar setData() com uma QVariant, mas se um ponteiro para void for necessário, pode ser obtido de forma trivial herdando uma classe de item e adicionando uma variável do tipo ponteiro para void. Para requisitos mais complexos de controle de dados, como grande data sets, classes de itens complexas, integração de banco de dados e visualizações múltiplas de dados, o Qt fornece uma série de classes de modelo e visualização que separam a data de sua representação visual. Esse assunto será abordado no Capítulo 10. • Caso a fórmula inicie com um sinal de igual (‘=’), a fórmula é interpretada como uma fórmula aritmética. Por exemplo, se a célula A1 contém “12” e a célula A2 contém“6”, a fórmula “=A1+A2” retorna 18. A tarefa de converter uma fórmula em um valor é realizado pela classe Cell. No momento, o que deve se manter em mente é que o texto exibido na célula é o resultado da fórmula, e não a fórmula em si. void Spreadsheet::setFormula(int row, int column, const QString &formula) { Cell *c = cell(row, column); if (!c) { c = new Cell; setItem(row, column, c); } c->setFormula(formula); } A função privada setFormula() habilita a fórmula para uma dada célula. Caso a célula já possua um objeto Cell, deve-se reutilizá-la. Caso contrário, criamos um novo objeto do tipo Cell e chamamos QTableWidget::setItem() para inseri-la dentro da tabela. No final, chamamos a própria função setFormula() da célula, que fará com que a célula seja remodelada caso seja mostrada na tela. Não precisamos nos preocupar em deletar o objeto Cell mais tarde; QTableWidget toma posse da célula e irá deletá-la automaticamente na hora certa. QString Spreadsheet::currentLocation() const { return QChar('A' + currentColumn()) + QString::number(currentRow() + 1); } A função currentLocation() retorna a localização atual da célula no formato usual da planilha, coluna de letras seguido por linhas enumeradas. MainWindows::updateStatusBar() o usa para mostrar a localização na barra de status. QString Spreadsheet::currentFormula() const { return formula(currentRow(), currentColumn()); } A função currentFormula() retorna a fórmula da célula passada. Ela é chamada em MainWindow::updateStatusBar().
  • 9. void Spreadsheet::somethingChanged() { if (autoRecalc) recalculate(); emit modified(); } O slot privado somethingChanged() recalcula a planilha inteira caso “auto-recalculate” esteja habilitado. Emite um sinal modified(). Carregando e Salvando Vamos agora implementar a opção de Salvar e Carregar em arquivos da Planilha utilizando um formato binário personalizado. Faremos isto usando QFile e QDataStream, que juntos fornecem Entrada e Saída de dados binários, independente da plataforma. Começaremos escrevendo um arquivo de Planilha: Visão do Código: bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot write file %1:n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream out(&file); out.setVersion(QDataStream::Qt_4_3); out << quint32(MagicNumber); QApplication::setOverrideCursor(Qt::WaitCursor); for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << quint16(row) << quint16(column) << str; } } QApplication::restoreOverrideCursor(); return true; } A função WriteFile() é executada de MinWindow::saveFile() para escrever o arquivo no disco. Retorna true se tudo der certo, e false em casos de erro.
  • 10. Criamos um objeto QFile com o nome dado e chamamos open() para abrir o arquivo para escrita. Também criamos um objeto do tipo QDataStream que opera no arquivo QFile e o usa para processar os dados. Antes de escrever os dados, mudamos o cursor da aplicação para o cursos de espera padrão ( ampulheta, geralmente) e o restauramos para o desenho normal assim que toda a data for escrita. No final da função, o arquivo é fechado automaticamente pelo destrutor de QFile. QDataStream suporta os tipos básicos de C++ assim como muitos dos tipos em Qt. A sintaxe é modelada depois das classes padrão C++ <iostream>. Por exemplo, Out << x << y << z; Escreve as variáveis x,y e z em um fluxo, e In >> x >> y >> z; As lê do fluxo. Devido ao fato dos tipos primitivos inteiros de C++ terem tamanhos diferentes em plataformas diferentes, é mais seguro converter estes valores em valores do tipo qint8, qint16, qint32,qint32, qint64, e qint64, que são certamente estarão em um tamanho que eles propõem( em bits). A formato do arquivo da aplicação da Planilha é bem simples. Uma planilha inicia com um número de 32 bits o qual identifica o formato do arquivo(MagicNumber, definido como 0x7F51C883 em spreadsheet.h, um número aleatório arbitrário). Depois vem uma série de blocos, cada um contendo coluna, linha e fórmula de cada célula. Para economizar espaço, não escrevemos em células vazias. O formato é mostrado na figura 4.3: Figura 4.3: O formato do arquivo Planilha A representação binária precisa dos tipos de dados é determinada por QDataStream.Por exemplo, um quint16 é armazenado como dois bytes com o mais significativo à esquerda, e um QString como o tamanho da string seguido de caracteres Unicode. A representação binária dos tipos Qt tem evoluído bastante desde Qt 1.0. Tende-se a continuar evoluindo em releases futuros para manter sincronia com a evolução dos tipos existentes e permitir entrada de novos tipos Qt. Por padrão, QDataStream usa a mais recente versão do formato Binário (versão 9 em Qt 4.3), mas pode ser ajustado para ler versões mais antigas. Para evitar problemas de compatibilidade caso a aplicação seja recompilada mais tarde usando um release mais atual, dizemos QDataStream para utilizar versão 9 independentemente da versão de Qt que estivermos compilando. (QDatasStream::Qt_4_3 é uma constante que resulta em 9.) QDataStream é muito versátil. Pode ser usada em um arquivo QFile, e também em um QBuffer, um QProcess, um QTcpSocket, um QUdpSocket, ou um QSslSocket. Qt também oferece uma classe QTextStream que pode ser usada ao invés de QDataStream para ler/escrever arquivos de texto. O capítulo 12 explica essas classes mais detalhadamente, e também descreve várias abordagens para controlar versões diferentes de QDataStream. Código:
  • 11. bool Spreadsheet::readFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot read file %1:n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream in(&file); in.setVersion(QDataStream::Qt_4_3); quint32 magic; in >> magic; if (magic != MagicNumber) { QMessageBox::warning(this, tr("Spreadsheet"), tr("The file is not a Spreadsheet file.")); return false; } clear(); quint16 row; quint16 column; QString str; QApplication::setOverrideCursor(Qt::WaitCursor); while (!in.atEnd()) { in >> row >> column >> str; setFormula(row, column, str); } QApplication::restoreOverrideCursor(); return true; } A função readFile()é muito similar à writeFile(. Usamos QFile para ler no arquivo, mas dessa vez usando a flag QIODevice::ReadOnly ao invés de QIODevice::WriteOnly. Depois setamos a versão de QDataStream para 9. O formato para leitura deve ser sempre o mesmo para escrita. Se o arquivo tem o número mágico correto no início, chamamos clear() para limpar todas as células da planilha, e lemos nos dados da célula. Já que o arquivo contém apenas os dados de células não-vazias, e é muito improvável que todas as células na planilha sejam definidas, temos de assegurar que todas as células sejam removidas antes da leitura.
  • 12. Implementando o Menu Editar Estamos prontos para implementar os slots que correspondem ao menu Edit da aplicação. O menu é mostrado na Figura 4.4. Figura 4.4. O Menu Edit da aplicação da Planilha void Spreadsheet::cut() { copy(); del(); } O slot cut() corresponde a Edit|Cut. A implementação é simples já que Cut é o mesmo de Copy, seguido por Delete. void Spreadsheet::copy() { QTableWidgetSelectionRange range = selectedRange(); QString str; for (int i = 0; i < range.rowCount(); ++i) { if (i > 0) str += "n"; for (int j = 0; j < range.columnCount(); ++j) { if (j > 0) str += "t"; str += formula(range.topRow()+i,range.leftColumn() +j); } } QApplication::clipboard()->setText(str); }
  • 13. O slot copy() corresponde a Edit|Copy. Ele age sobre a área selecionada ( que se torna a célula atual, caso nada tenha sido selecionado). Cada célula selecionada é adicionada a uma QString, com linhas separadas por caracteres de nova linha, e colunas separadas por caracteres de parágrafo. Isto é ilustrado na Figure 4.5. Figura 4.5. Copiando uma seleção para Área de Transferência A área de transferência do sistema está disponível em Qt através da função estátitca QApplication::clipboard(). Através da chamada de QClipboard::setText(), deixamos o texto disponível na área de transferência, não só para esta aplicação mas também para as demais aplicações que suportem texto simples. Nosso formato, que utiliza caracteres de parágrafo e nova linha como separadores, é identificado por uma série de aplicações, incluindo Microsoft Excel. A função QTableWidget::selectedRanges() retorna uma lista de comprimentos de seleção. Sabemos que não poderá existir mais de um, pois setamos o modo de seleção para QAbstractItemView::ContiguousSelection no construtor. Para nossa conveniência, definimos uma função selectedRange() que retorna o comprimento da seleção: QTableWidgetSelectionRange Spreadsheet::selectedRange() const { QList<QTableWidgetSelectionRange> ranges = selectedRanges(); if (ranges.isEmpty()) return QTableWidgetSelectionRange(); return ranges.first(); } Se há uma seleção de tudo, basta devolver a primeira (e única). Deve sempre haver uma seleção já que o modo ContiguousSelection trata a célula atual como sendo selecionada. Mas para proteger contra a possibilidade de um bug no nosso programa que não faz nenhuma célula ser a atual, nós tratamos este caso. Código: void Spreadsheet::paste() { QTableWidgetSelectionRange range = selectedRange(); QString str = QApplication::clipboard()->text(); QStringList rows = str.split('n'); int numRows = rows.count(); int numColumns = rows.first().count('t') + 1;
  • 14. if (range.rowCount() * range.columnCount() != 1 && (range.rowCount() != numRows || range.columnCount() != numColumns)) { QMessageBox::information(this, tr("Spreadsheet"), tr("The information cannot be pasted because the copy " "and paste areas aren't the same size.")); return; } for (int i = 0; i < numRows; ++i) { QStringList columns = rows[i].split('t'); for (int j = 0; j < numColumns; ++j) { int row = range.topRow() + i; int column = range.leftColumn() + j; if (row < RowCount && column < ColumnCount) setFormula(row, column, columns[j]); } } somethingChanged(); } O slot paste() corresponde a Edit|Paste. Buscamos o texto na Área de Transferência e chamamos a função estática QString::split() para quebrar a string em um QStringList. Cada linha se torna uma string na lista. Depois, determinamos as dimensões da área de cópia. O número de linhas é o número de strings na QStringList; o número de colunas é o número de caracteres Parágrafo na primeira linha, mais 1. Se apenas uma célula for selecionada, usamos essa célula como o canto esquerdo superior da área de cola; caso haja mais de uma célula, usamos a seleção como área de cola. Para realizar a ação de colar, permutamos as linhas e as dividimos em células usando QString::split() novamente, mas dessa vez usando parágrafo como separador. A Figura 4.6 ilustra as etapas. Figura 4.6. Colando texto da área de transferência na Planilha. void Spreadsheet::del() { QList<QTableWidgetItem *> items = selectedItems(); if (!items.isEmpty()) { foreach (QTableWidgetItem *item, items) delete item; somethingChanged(); }
  • 15. } O slot del() corresponde a Edit|Delete. Se houver itens selecionados, a função os apaga e faz uma chamada para somethingChanged(). É suficiente usar delete em cada objeto Cell na seleção para limpar células. QTableWidget notifica quando seus itens de QTableWidgetItem são deletados e automaticamente se re-pinta caso algum dos itens esteja disponível. Se chamarmos cell() com a localização de uma célula deletada, vai retornar um ponteiro para null. void Spreadsheet::selectCurrentRow() { selectRow(currentRow()); } void Spreadsheet::selectCurrentColumn() { selectColumn(currentColumn()); } As funções selectCurrentRow() e selectCurrentColumn() corresponde às opções Edit|Select|Row e Edit|Select|Column no menu. As implementações são baseadas nas funções de QTableWidget, selectRow() e selectColumn(). Não precisamos implementar a funcionalidade por trás de Edit|Select|All, já que é fornecida pela função herdada de QTableWidget, QAbstractItemView::selectAll(). void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs) { int row = currentRow(); int column = currentColumn() + 1; while (row < RowCount) { while (column < ColumnCount) { if (text(row, column).contains(str, cs)) { clearSelection(); setCurrentCell(row, column); activateWindow(); return; } ++column; } column = 0; ++row; } QApplication::beep(); } O slot findText() varre através das células iniciando da célula à direita do cursor, se movendo pela direita até a última coluna, e depois continua da primeira coluna da linha abaixo, e assim em diante até o texto ser encontrado ou até atingir a última célula. Por exemplo, se a célula atual for C24, pesquisamos D24, E24,..., Z24, A25, B25,..., Z25, até Z999. Se encontrarmos o texto correspondente, limpamos a seleção atual, movemos o cursor de células até a célula contendo o texto encontrado, e ativamos a janela que contém Spreadsheet. Se não for encontrado o texto, fazemos a aplicação emitir um alerta, avisando que a busca encerrou sem sucesso. void Spreadsheet::findPrevious(const QString &str, Qt::CaseSensitivity cs) { int row = currentRow(); int column = currentColumn() - 1; while (row >= 0) {
  • 16. while (column >= 0) { if (text(row, column).contains(str, cs)) { clearSelection(); setCurrentCell(row, column); activateWindow(); return; } --column; } column = ColumnCount - 1; --row; } QApplication::beep(); } O slot findPrevious() é similar ao slot findText(), exceto que ele pesquisa de frente para trás, e pára na célula A1. Implementando os Outros Menus Vamos agora implementar os slots para os menus Tools e Options. Esses menus são exibidos na Figura 4.7. Figura 4.7. Os menus Tools e Options da aplicação da Planilha. void Spreadsheet::recalculate() { for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { if (cell(row, column)) cell(row, column)->setDirty(); } } viewport()->update(); } O slot recalculate() corresponde a Tools|Recalculate. Também é chamado automaticamente por Spreadsheet quando necessário. Vasculhamos as células e chamamos setDirty() em cada célula para marcar as que serão recalculadas. Da próxima vez que QTableWidget chamar text() em uma Cell para obter o valor para mostrar na planilha, o valor será recalculado. Depois chamamos update() no viewport para editar a planilha inteira. O código de edição em QTableWidget chama text() em cada célula visível para obter o valor para exibir. Já que fizemos chamada a setDirty() em cada célula, as chamadas a text() usarão um valor recém-calculado. O cálculo pode requerer células não-visíveis para serem recalculadas, cascateando o cálculo até que cada célula que precisa ser
  • 17. recalculada para mostrar o valor correto seja recalculada. O cálculo é feito pela classe Cell. void Spreadsheet::setAutoRecalculate(bool recalc) { autoRecalc = recalc; if (autoRecalc) recalculate(); } O slot setAutoRecalculate() corresponde a Options|Auto-Recalculate. Se estiver ativado, faz com que seja recalculada a planilha inteira imediatamente para assegurar que está atualizada; depois, recalculate() é chamado automaticamente de somethingChanged(). Não precisamos implementar nada para Options|Show Grid já que QTableWidget possui um slot setShowGrid(), que é herdado de QTableView. Resta apenas Spreadsheet::sort(), vindo de MainWindow::sort(): void Spreadsheet::sort(const SpreadsheetCompare &compare) { QList<QStringList> rows; QTableWidgetSelectionRange range = selectedRange(); int i; for (i = 0; i < range.rowCount(); ++i) { QStringList row; for (int j = 0; j < range.columnCount(); ++j) row.append(formula(range.topRow() + i, range.leftColumn() + j)); rows.append(row); } qStableSort(rows.begin(), rows.end(), compare); for (i = 0; i < range.rowCount(); ++i) { for (int j = 0; j < range.columnCount(); ++j) setFormula(range.topRow() + i, range.leftColumn() + j, rows[i][j]); } clearSelection(); somethingChanged(); } Ordenação opera na seleção feita e reordena as linhas de acordo com as chaves de ordenação e ordens de ordenação, armazenados no objeto compare. Representamos cada linha de dados com uma QStringList e armazenamos a seleção como uma lista de linhas. Usamos o algoritmo qStableSort(), advindo do Qt, e para simplificar sorteamos por fórmula, ao invés de valor. O processo é ilustrado nas Figuras 4.8 e 4.9. Cobrimos os algoritmos-padrão e estruturas de dados para Qt no Capítulo 11. Figura 4.8. Armazenando a seleção como uma lista de linhas
  • 18. Figura 4.9. Colocando dados de volta na tabela, após ordenação A função qStableSort() aceita um iterador inicial, um iterador final, e uma função de comparação. A função de comparação é uma função que leva dois argumentos(duas QStringsList) e retorna true caso o primeiro argumento é “menor que” o segundo argumento, e false caso contrário. O objeto compare que passamos como função comparadora não é realmente uma função, mas pode ser usado como uma, como veremos resumidamente. Depois de realizar QStableSOrt(), movemos os dados de volta à tabela, limpamos a seleção, e chamamos somethingChanged(). Em Spreadsheet.h, c classe SpdeadsheetCompare foi definida de seguinte forma: class SpreadsheetCompare { public: bool operator()(const QStringList &row1, const QStringList &row2) const; enum { KeyCount = 3 }; int keys[KeyCount]; bool ascending[KeyCount]; }; A classe SpreadSheetCompare é especial porque implementa o operador (). Isto nos permite utilizar a classe como se fosse uma função. Tais classes são chamadas function objects, ou functors. Oara entender como functors atuam, iniciemos com um exemplo simples: class Square { public: int operator()(int x) const { return x * x; } } A ckasse Square fornece uma função, operator() (int), que retorna o quadrado de seu parâmetro. Nomeando a função como operator() (int), ao invés de compute(int), por exemplo, ganha-se a capcidade de se usar um objeto do tipo Square como se ele fosse uma função:
  • 19. Square square; int y = square(5); // y equals 25 Agora vejamos um exemplo envolvendo SpreadsheetCompare: QStringList row1, row2; SpreadsheetCompare compare; if (compare(row1, row2)) { // row1 is less than row2 } O objeto compare pode ser usado bem como se fosse uma simples função compare(). Adicionalmente, sua implementação pode acessar todas as chaves e ordens de combinações, que são armazenadas como variáveis membros. Uma alternativa para este esquema seria armazenar chaves e ordens em variáveis globais, e usar um função simples compare(). Entretanto, não é muito elegante se comunicar entre variáveis globais, além do risco de bugs. Functors são uma forma mais forte para criar uma interface com as funções do tipo template, como qStableSort(); Aqui temos uma implementação da função que é usada para comparar duas linhas de planilhas: bool SpreadsheetCompare::operator()(const QStringList &row1, const QStringList &row2) const { for (int i = 0; i < KeyCount; ++i) { int column = keys[i]; if (column != -1) { if (row1[column] != row2[column]) { if (ascending[i]) { return row1[column] < row2[column]; } else { return row1[column] > row2[column]; } } } } return false; } O operador retorna true caso a primeira linha seja menor do que a segunda linha; do contrário, retorna false. A função qStableSort() usa o resultado desta função para realizar a ordenação. As Keys dos objetos de SpreadsheetCompare as arrays ascending são povoadas na função MainWindow::sort() ( mostrada no capítulo 2). Cada chave possui um endereço de coluna, ou um valor -1 ( “Nada”). Comparamos as entradas da célula correspondente nas duas linhas para cada chave em ordem. Assim que encontramos uma diferença, retornamos um valor apropriado true ou false. Se todas as comparações acabam por ser iguais, voltamos o valor false. A função qStableSort ()usa a ordem antes da classificação para resolver situações de empate; se row1 precedeu row2 originalmente e nem se compara como "inferior"
  • 20. ao outro, row1 ainda precede row2 no resultado. Isto distingue qStableSort () do seu primo instável qsort (). Completamos agora a classe Spreadsheet. Na próxima sessão, vamos revisar a classe Cell. Essa classe é usada para guardar fórmulas de células e fornece uma reimplementação da função QTableWidgetItem::data(), que é chamada indiretamente por Spreadshet, através da função QTableWidgetIetm::text(), para exibir o resultado do cálculo em uma célula. A Subclasse QTableWidgetItem A classe Cell é derivada de QTableWidgetItem. A classe é designada para trabalhar bem como Spreadsheet, mas não possui dependências específicas naquela classe e, em teoria, pode ser usada em qualquer QTableWidget. Aqui está o arquivo cabeçalho: Código: #ifndef CELL_H #define CELL_H #include <QTableWidgetItem> class Cell : public QTableWidgetItem { public: Cell(); QTableWidgetItem *clone() const; void setData(int role, const QVariant &value); QVariant data(int role) const; void setFormula(const QString &formula); QString formula() const; void setDirty(); private: QVariant value() const; QVariant evalExpression(const QString &str, int &pos) const; QVariant evalTerm(const QString &str, int &pos) const; QVariant evalFactor(const QString &str, int &pos) const; mutable QVariant cachedValue; mutable bool cacheIsDirty; }; #endif A Classe cell estende QTableWidgetItem através da adição de duas variáveis provadas: • cachedValue armazena em cachê o valor da célula como um QVariant.
  • 21. • cachedIsDirty, valor booleano que é true caso o valor guardado em cachê não está atualizado. Usamos QVariant porque algumas células apresentam valores do tipo Double, enquanto outras possuem valor do tipo QString. As variáveis cachedValue e cacheIsDirty são declaradas com a palavra-chave mutable do C++. Isto permite-nos modificar essas variáveis em funções const. Alternativamente, recalcularíamos o valor cada vez que text() é chamado, um procedimento um tanto ineficiente. Note que não há, na definição da classe, um macro Q_OBJECT. Cell é uma classe C++ simples, sem sinais ou slots. Na verdade, já que QTableWidget não é derivada de QObject, nunca poderemos ter sinais nem slots na classe Cell enquanto ela estiver viva. Classes de itens do Qt não são derivadas de QObject afim de se manter seu custo o menor possível. Se forem necessários sinais e slots, podem ser implementados no widget que contiver os itens ou, excepcionalmente, usando herança múltipla com QObject. Aqui está o início de cell.cpp: #include <QtGui> #include “cell.h” Cell::cell() { setDirty(); } No construtor, apenas precisamos marcar a cachê como dirty. Não há necessidade de passar uma superclasse; quando a célula é inserida em um QTableWidget com setItem(), o QTableWidget vai tomar posse dele automaticamente. Toda QTableWidgetItem pode guardar algum dados, que podem ser no máximo um QVariant para cada “papel” que cada dado possui. Os papéis mais assumidos são Qt::EditRole e Qt::DisplayRole. Papel de edição é usado para dados que estão para serem editados, e o papel de exibição, para dados que estão para serem exibidos. Freqüentemente os dados para ambos são os mesmos, mas na classe Cell o papel de edição corresponde às fórmulas das células e a tarefa de display corresponde ao valor da célula (o resultado da avaliação da fórmula). QTableWidgetItem *Cell::clone() const { return new Cell(*this); } A função clone()é chamada por QTableWidget quando necessita criar uma nova célula – por exemplo, quando o usuário começa a digitar em uma célula vazia que nunca foi usada antes. A instância passada para QTableWIdget::setItemPrototype() é o item que é clonado. Já que uma cópia do melhor membro é suficiente para Cell, estamos confiando no construtor de cópia padrão automaticamente criado pelo C++ para criar novas instancias de Cell na função clone(). void Cell::setFormula(const QString &formula)
  • 22. { setData(Qt::EditRole, formula); } A função setFormula() determina a fórmula da célula. É simplesmente uma função de conveniência que chama setData() com a tarefa de editar. Ela vem de Spreadsheet::setFormula(). QString Cell::formula() const { return data(Qt::EditRole).toString(); } A função formula() é chamada de Spreadsheet::formula(). Assim como setFormula(), é uma função de conveniência, desta vez recuperando a data do item em Edit Role. void Cell::setData(int role, const QVariant &value) { QTableWidgetItem::setData(role, value); if (role == Qt::EditRole) setDirty(); } Se tivermos uma nova fórmula, marcamos true em cacheIsDirty para assegurar que a célula é recalculada da próxima vez que text() for chamada. Não existe uma função text() definida em Cell, apesar de chamarmos texxt() em instâncias de Cell em Spreadsheet::text(). A função text() é uma conveniência dada por QTableWidgetItem; é o equivalente a chamar data(Qt::DisplaRole).toString(). void Cell::setDirty() { cacheIsDirty = true; } A função setDirty() é chamada para forçar um re-cálculo do valor da célula. Ele simplesmente habilita true em cacheIsDirty, o que significa que cachedValue não estará mais atualizado. O re-cálculo não é realizado enquanto não for necessário. QVariant Cell::data(int role) const { if (role == Qt::DisplayRole) { if (value().isValid()) { return value().toString(); } else { return "####"; } } else if (role == Qt::TextAlignmentRole) { if (value().type() == QVariant::String) { return int(Qt::AlignLeft | Qt::AlignVCenter); } else { return int(Qt::AlignRight | Qt::AlignVCenter); } } else { return QTableWidgetItem::data(role); } } A função data() é re-implementada de QTableWidgetItem. Ela retorna o texto que deve ser mostrado na planilha se chamado com Qt::DissplayRole, e a fórmula se
  • 23. chamado com Qt::EditRole. Retorna um alinhamento apropriado se chamado com Qt::TextAlignmentRole. No caso de DisplayRole, ela depende de value() para computar o valor da célula. Caso o valor seja inválido (decorrente de fórmula incorreta), retornamos “####”. A função Cell::value() usada em data() retorna um QVariant. Um QVariant pode armazenar diversos valores de diferentes tipos, como Double e QString, e fornece funções para conversão da variante em outros tipos. Por exemplo, chamar toString() em uma variante que guarda um valor double produz uma representação em string do valor em double. Um QVariant construído usando um construtor default é um variante “inválido”. Código: const QVariant Invalid; QVariant Cell::value() const { if (cacheIsDirty) { cacheIsDirty = false; QString formulaStr = formula(); if (formulaStr.startsWith(''')) { cachedValue = formulaStr.mid(1); } else if (formulaStr.startsWith('=')) { cachedValue = Invalid; QString expr = formulaStr.mid(1); expr.replace(" ", ""); expr.append(QChar::Null); int pos = 0; cachedValue = evalExpression(expr, pos); if (expr[pos] != QChar::Null) cachedValue = Invalid; } else { bool ok; double d = formulaStr.toDouble(&ok); if (ok) { cachedValue = d; } else { cachedValue = formulaStr; } } Return cachedValue; } A função privada value() retorna o valor da célula. Caso cacheIsDirty seja true, precisamos recalcular o valor. Se a fórmula começa com um apóstrofo( e.g., “’12345”), tomamos a string a partir da posição 1 e removemos quaisquer espaços que ela possa conter. Depois, chamamos evalExpression() para computar o valor da expressão. O argumento pos é passado por referência; indica a posição do caractere onde a análise deve começar. Após a chamada para evalExpression(), o caractere para posição pos deve ser o caractere QChar::Null que anexamos, caso tenha sido analisado com sucesso. Caso a análise tenha falhado antes do fim, ajustamos cacheValue para Invalid. Se a fórmula não começar com aspa simples ou um sinal de igual, devemos tentar convertê-la em um valor ponto-flutuante, usando toDouble(). Caso a conversão funcione, ajustamos cachedValue para ser o valor resultante; se não funcionar,
  • 24. ajustamos cachedValue para ser a string da fórmula. Por exemplo, uma fórmula de “1.50” faz com que toDouble() marque ok em true e retorne 1.5, enquanto que a fórmula de “World Population” faz com que toDouble() marque ok em false, e retorne 0.0. Ao dar um ponteiro para bool a toDouble(), se torna possível distinguir entre a conversão de uma string que representa o valor numérico 0.0 e um erro de conversão ( onde 0.0 também é retornado, porém o valor bool está marcado como false). Ás vezes o fato de se retornar um valor zero em erro de conversão é exatamente o que precisamos, já que no caso não nos preocupamos em passar um ponteiro para bool. Por razões de portabilidade e performance, Qt nunca usa exceções do C++ para reportar falha. Isso não o previne de usá-las em programas Qt caso seu compilador as suporte. A função value() é declarada constante. Tivemos de declarar cachedValue e cacheIsValid como variáveis mutáveis para que o compilador nos permita modificá-las em funções constantes. Pode ser tentador fazer value() uma função não-constante e remover as keywords mutable, mas isso não compilaria, pois chamamos value)_ de data(), uma função constante. Terminamos assim a aplicação Spreadsheet, incluindo a análise de fórmulas. O restante desta seção cobre evalExpression() e outras duas funções de caráter de ajuda, evalTerm() e evalFactor(). O código é um tanto complicado, mas consta aqui para tornar a aplicação completa. Devido ao fato do código não estar relacionado à programação GUI, você pode seguramente pular esta etapa e continuar a leitura no Capítulo 5. A função evalExpression() retorna o valor da expressão de uma planilha. Uma expressão é definida como um ou mais termos separados por ‘+’ ou ‘-‘. Os termos são definidos como um ou mais fatores, separados por operadores ‘*’ ou ‘/’. Pela quebra de expressões em termos, e, consequentemente, em fatores, asseguramos que os operadores são aplicados com a precedência correta. Por exemplo, “2*C5+D6” é um expressão cujo primeiro termo é “2*C5”, e o segundo termo é “D6”. O Termo “2*C5” possui “2’ como primeiro fator, e “C5” como segundo fator, e o termo “D6” consiste em um fator singular. Um fator pode ser um número ( “2”), uma localização de célula (“C5”), ou uma expressão em parênteses, opcionalmente precedida por um unário “-“. A sintaxe de expressões da planilha é definida na Figura 4.10. Para cada símbolo na gramática (Expressão, Termo e Fator), existe uma função-membro correspondente que o analisa e cuja estrutura segue fielmente a gramática. Analisadores escritos dessa forma são chamados analisadores recursivos descententes. Figura 4.10. Diagrama de Sintaxe para expressões da planilha
  • 25. Vamos começar com evalExpression(), a função que analisa uma Expression: Código: QVariant Cell::evalExpression(const QString &str, int &pos) const { QVariant result = evalTerm(str, pos); while (str[pos] != QChar::Null) { QChar op = str[pos]; if (op != '+' && op != '-') return result; ++pos; QVariant term = evalTerm(str, pos); if (result.type() == QVariant::Double && term.type() == QVariant::Double) { if (op == '+') { result = result.toDouble() + term.toDouble(); } else { result = result.toDouble() - term.toDouble(); } } else { result = Invalid; } } return result; } Primeiro, chamamos evalTerm() para obter o valor do primeiro termo. Se o caractere seguinte for ‘+’ ou ‘-‘, continuamos chamano evalTerm() mais uma vez; do contrário, a expressão consiste em um único termo, e retornamos seu valor como o valor da expressão inteira. Após obtermos o valor dos dois primeiros termos, computamos o resultado da operação, de acordo com o operador. Caso ambos os termos tenham sido levados a um tipo double, computamos o resultado como um double; do contrário, setamos o resultado como Invalid. Continuamos dessa forma até que não haja mais termos. Isto funciona corretamente pois adição e subtração são associativas à esquerda; ou seja, “1-2-3” significa “(1-2)-3”, e não “1-(2-3)”.
  • 26. Código: QVariant Cell::evalTerm(const QString &str, int &pos) const { QVariant result = evalFactor(str, pos); while (str[pos] != QChar::Null) { QChar op = str[pos]; if (op != '*' && op != '/') return result; ++pos; QVariant factor = evalFactor(str, pos); if (result.type() == QVariant::Double && factor.type() == QVariant::Double) { if (op == '*') { result = result.toDouble() * factor.toDouble(); } else { if (factor.toDouble() == 0.0) { result = Invalid; } else { result = result.toDouble() / factor.toDouble(); } } } else { result = Invalid; } } return result; } A função evalTerm() é muito similar à evalExpression(), exceto pelo fato de que trabalha com multiplicação e divisão. A única sutileza em evalTerm() é que devemos tratar a divisão por zero, já que é um erro em alguns processadores. Já que é desaconselhável testar valores em ponto flutuante para igualdade, devido a erros de arredondamento, é mais seguro testar igualdade em relação a 0.0 para se evitar divisão por zero. Código: QVariant Cell::evalFactor(const QString &str, int &pos) const { QVariant result; bool negative = false; if (str[pos] == '-') { negative = true; ++pos; } if (str[pos] == '(') { ++pos; result = evalExpression(str, pos); if (str[pos] != ')') result = Invalid; ++pos; } else { QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}"); QString token; while (str[pos].isLetterOrNumber() || str[pos] == '.') { token += str[pos]; ++pos; } if (regExp.exactMatch(token)) { int column = token[0].toUpper().unicode() - 'A'; int row = token.mid(1).toInt() - 1; Cell *c = static_cast<Cell *>(
  • 27. tableWidget()->item(row, column)); if (c) { result = c->value(); } else { result = 0.0; } } else { bool ok; result = token.toDouble(&ok); if (!ok) result = Invalid; } } if (negative) { if (result.type() == QVariant::Double) { result = -result.toDouble(); } else { result = Invalid; } } return result; } A função evalFactor() é um pouco mais complicada do que evalExpression() e evalTerm().Começamos constatando onde o fator é negado. Então, vemos se este trecho começa com um parênteses abertos. Se abrir, avaliamos o conteúdo de parênteses e como uma expressão através da chamada a evalExpression(). Quando analisa uma expressão em parênteses, evalExpression() faz uma chamada a evalTerm(), que chama evalFactor(), que por sua vez chama evalExpression() novamente. É aqui que a recursividade ocorre no analisador. Caso o fator não seja uma expressão aninhada, extraímos o próximo token, que deve ser uma localização de célula ou um número. Caso o token case com QRegExp, passamos a tratá-lo como um referência a célula e chamamos value() na célula que possui tal endereço. A célula pode estar em qualquer lugar na planilha, e pode ter dependências em outras células. As dependências não são um problema; elas vão simplesmente disparar mais chamadas a value() e ( para células “sujas”), mais análise até que todas os valores de células dependetes sejam calculados. Caso o token não seja uma localização de célula, o tratamos como um número. O que acontece se a célula A1 contem a fórmula “=A1”? Ou se a célula A1 contem “=A2” e a célula A2 contem “=A1”? Apesar de não termos escrito nenhum código especial para detectar dependências cíclicas, o analisador controla estes casos prontamente, retornando um inválido QVariant. Isto funciona porque ajustamos cacheisDirty para false e cachedValue para Invalid em value() antes de chamarmos evalExpression(). Caso evalExpression() chame recursivamente value() na mesma célula, ela retorna Invalid imediatamente, e a expressão por inteiro se torna Invalid. Completamos, enfim, o analisador de fórmula. Seria justo estender isto para funções predefinidas na planilha, como “sum()” e “avg()”, através da extensão da definição gramatical de Fator. Outra extensão simples seria a implementação do operador de “+” com strings como operandos ( ou seja, uma concatenação); isto não requer mudanças na gramática.