27
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.

Cap4

Embed Size (px)

Citation preview

Page 1: Cap4

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.

Page 2: Cap4

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, ecada 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 deque 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.

Page 3: Cap4

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

Page 4: Cap4

classe Cell é derivada de QTableWidgetItem e serão explicados na última sessão deste capítulo.

class Spreadsheet : public QTableWidget{ Q_OBJECTpublic: 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);

Page 5: Cap4

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);

Page 6: Cap4

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));}

Page 7: Cap4

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 umvalor 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 emelementos 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

Page 8: Cap4

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, quefará 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().

Page 9: Cap4

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.

Page 10: Cap4

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 noarquivo 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 cadacé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 controlarversões diferentes de QDataStream.

Código:

Page 11: Cap4

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 daplanilha, 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.

Page 12: Cap4

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);}

Page 13: Cap4

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ãosó 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 deseleção. Sabemos que não poderá existir mais de um, pois setamos o modo de seleção para QAbstractItemView::ContiguousSelection no construtor. Para nossaconveniê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á queo modo ContiguousSelection trata a célula atual como sendo selecionada. Mas para proteger contra a possibilidadede 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;

Page 14: Cap4

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ênciae 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 umacé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(); }

Page 15: Cap4

}

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 casoalgum 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çõesEdit|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 umalerta, 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) {

Page 16: Cap4

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 paraexibir. 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

Page 17: Cap4

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çãocomo 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

Page 18: Cap4

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 decomparaçã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 passamoscomo 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 nospermite 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:

Page 19: Cap4

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 pararealizar a ordenação.

As Keys dos objetos de SpreadsheetCompare as arrays ascending são povoadas nafunçã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"

Page 20: Cap4

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;

};

#endifA 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.

Page 21: Cap4

• 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á necessidadede 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)

Page 22: Cap4

{ setData(Qt::EditRole, formula);}A função setFormula() determina a fórmula da célula. É simplesmente uma funçãode 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ãoestará 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

Page 23: Cap4

chamado com Qt::EditRole. Retorna um alinhamento apropriado se chamado comQt::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,

Page 24: Cap4

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” comosegundo fator, e o termo “D6” consiste em um fator singular. Um fator pode ser umnú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

Page 25: Cap4

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 caractereseguinte 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)”.

Page 26: Cap4

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 devemostratar 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 *>(

Page 27: Cap4

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. Casoo 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.