29
Parte II: Qt Intermediário 6. Gerenciamento do Layout Planejando os Widgets em um Form Layouts Empilhados Splitters Áreas de Rolagem Encurtando Janelas e Barra de Ferramentas Interface Múltipla de Documento Todo widget que é colocado em um form deve receber um tamanho e posição apropriados. O Qt fornece diversas classes que ajustam widgets em um Form: QHBoxLayout, QVBoxLayout, QGridLayout, e QSTackLayout. Essas classes são tão convenientes e fáceis de se usar que quase todo desenvolvedor Qt as usa, seja diretamente no código fonte , ou através do Qt Designer. Outra razão para se usar classes de layout do Qt é que elas garantem que as formas se adaptem automaticamente para diferentes formas, linguagens e plataformas. Caso o usuário mude as configurações de fonte do sistema, as Forms da aplicação irão se adaptar automaticamente, redimensionando caso seja necessário. Se você traduzir a interface da aplicação para outro idioma, as clases de layout levam em consideração os conteúdos traduzidos das widgets para evitar entroncamento do texto. Outras classes que realizam manutenção de layout incluem QSplitter, QScrollArea, QMainWindow, e QMdiArea. Todas essas classes fornecem um layout flexível que o usuário pode manipular. Por exemplo, QSplitter fornece uma barra separador a que o usuário pode arrastar para redimensionar widgets, e QMdiArea oferece suporte à MDI ( Multiple Document Interface), uma maneira de mostrar diversos documentos

Cap6

Embed Size (px)

Citation preview

Page 1: Cap6

Parte II: Qt Intermediário

6. Gerenciamento do Layout

Planejando os Widgets em um Form

Layouts Empilhados

Splitters

Áreas de Rolagem

Encurtando Janelas e Barra de Ferramentas

Interface Múltipla de Documento

Todo widget que é colocado em um form deve receber um tamanho e posição

apropriados. O Qt fornece diversas classes que ajustam widgets em um Form:

QHBoxLayout, QVBoxLayout, QGridLayout, e QSTackLayout. Essas classes são tão convenientes e

fáceis de se usar que quase todo desenvolvedor Qt as usa, seja diretamente no

código fonte , ou através do Qt Designer.

Outra razão para se usar classes de layout do Qt é que elas garantem que as

formas se adaptem automaticamente para diferentes formas, linguagens e

plataformas. Caso o usuário mude as configurações de fonte do sistema, as Forms

da aplicação irão se adaptar automaticamente, redimensionando caso seja

necessário. Se você traduzir a interface da aplicação para outro idioma, as clases

de layout levam em consideração os conteúdos traduzidos das widgets para evitar

entroncamento do texto.

Outras classes que realizam manutenção de layout incluem QSplitter, QScrollArea,

QMainWindow, e QMdiArea. Todas essas classes fornecem um layout flexível que o

usuário pode manipular. Por exemplo, QSplitter fornece uma barra separador a que o

usuário pode arrastar para redimensionar widgets, e QMdiArea oferece suporte à MDI

( Multiple Document Interface), uma maneira de mostrar diversos documentos

Page 2: Cap6

simultaneamente dentro da janela principal de uma aplicação. Devido ao fato de

serem recentemente usadas como alternativas para as classes de layout

adequadas, vamos abordar essa técnica neste capítulo.

Planejando Widgets em um Form

Existem três formas básicas de gerenciar o layout em widgets menores num Form:

posicionamento absoluto, layout manual, e gerenciadores de layout. Vamos analisar cada abordagem de uma vez, usando a caixa de diálogo de Busca por Arquivos,

como mostra a Figura 6.1 abaixo.

Figura 6.1. A Caixa de diálogo Find File

Posicionamento absoluto é a forma mais crua de projetar widgets. E alcançável

através da atribuição de tamanhos e posições para as widgets-filhas do Form, além

de um tamanho fixo para o Form. O construtor de FindFileDialog usando

posicionamento absoluto fica assim:

FindFileDialog::FindFileDialog(QWidget *parent)

QDialog(parent)

{

..

Page 3: Cap6

namedLabel->setGeometry(9, 9, 50, 25);

namedLineEdit->setGeometry(65, 9, 200, 25);

lookInLabel->setGeometry(9, 40, 50, 25);

lookInLineEdit->setGeometry(65, 40, 200, 25);

subfoldersCheckBox->setGeometry(9, 71, 256, 23);

tableWidget->setGeometry(9, 100, 256, 100);

messageLabel->setGeometry(9, 206, 256, 25);

findButton->setGeometry(271, 9, 85, 32);

stopButton->setGeometry(271, 47, 85, 32);

closeButton->setGeometry(271, 84, 85, 32);

helpButton->setGeometry(271, 199, 85, 32);

setWindowTitle(tr("Find Files or Folders"));

setFixedSize(365, 240);

}

Posicionamento Absoluto possui diversas desvantagens:

O usuário nunca poderá redimensionar a janela;

Partes do texto podem ser truncadas se o usuário escolher um tipo de fonte muito largo ou se a aplicação for traduzida para outro idioma.

Os Widgets podem ter tamanhos inapropriados para alguns estilos.

As posições e tamanhos devem ser calculados manualmente. Isso é

entediante e sujeito a erros, além, de tornar manutenção um desafio.

Um alternativa para Posicionamento Absoluto é layout manual. Com Layout Manual,

ainda são dadas posições absolutas para os widgets, mas seus tamanhos são definidos proporcionais ao tamanho da janela, sendo desnecessária codificação

bruta.

Código:

Page 4: Cap6

FindFileDialog::FindFileDialog(QWidget *parent)

: QDialog(parent)

{

...

setMinimumSize(265, 190);

resize(365, 240);

}

void FindFileDialog::resizeEvent(QResizeEvent * /* event */)

{

int extraWidth = width() - minimumWidth();

int extraHeight = height() - minimumHeight();

namedLabel->setGeometry(9, 9, 50, 25);

namedLineEdit->setGeometry(65, 9, 100 + extraWidth, 25);

lookInLabel->setGeometry(9, 40, 50, 25);

lookInLineEdit->setGeometry(65, 40, 100 + extraWidth, 25);

subfoldersCheckBox->setGeometry(9, 71, 156 + extraWidth, 23);

tableWidget->setGeometry(9, 100, 156 + extraWidth,

50 + extraHeight);

messageLabel->setGeometry(9, 156 + extraHeight, 156 +

extraWidth, 25);

findButton->setGeometry(171 + extraWidth, 9, 85, 32);

stopButton->setGeometry(171 + extraWidth, 47, 85, 32);

closeButton->setGeometry(171 + extraWidth, 84, 85, 32);

helpButton->setGeometry(171 + extraWidth, 149 + extraHeight, 85,

32);

}

Page 5: Cap6

No construtor FindFileDialog , Ajustamos o tamanho mínimo do form para 265 X 190 e

o tamanho inicial para 365 X 240. No controlador resizeEvent(), damos o tamanho

extra à widgets conforme o quanto queremos expandi-la. Isso garante que o Form aumente gradualmente quando o usuário a redimensiona.

Assim como posicionamento absoluto, layout manual requer bastante codificação

de constantes para serem calculadas pelo programador. Codificação escrita dessa forma é canstiva, especialmente com mudanças de aspecto visual. Layout manual

também corre risco de sofrer entruncamento do texto. Podemois evitar isto

levando em consideração as sugestões de tamanho das widgets-filhas, mas isto

tornaria o código ainda mais complicado.

A forma mais conveniente de se solucionar problemas de layout de widgets em Qt é

o uso dos gerenciadores de layout. Os gerenciadores de layout fornecem padrões

sensíveis para cada tipo de widget e levam em conta as sugestões de tamanho de cada widget, o que depende do conteúdo, estilo e tamanho da fonte do widget.

Gerenciadores de layout também respeitam tamanhos mínimo e máximo, e

automaticamente ajustam o layout em resposta às mudanças de fonte, e de

conteúdo, além de redimensionamento de janela. Uma versão redimensionável do

Find File dialog é mostrado na Figura 6.2.

Figura 6.2. Redimensionando uma janela redimensionável

[Janela aumentada]

Os três gerenciadores de layout mais importantes são : QHBoxLayout, QVBoxLayout e

QGridLayout. Essas classes são derivadas de QLayout, que fornece o framework básico

para layouts. Todas as três classes são totalmente suportadas pelo Qt Designer e

podem ser usadas também diretamente em código.

Aqui está o código de FindFIleDialog usando Gerenciadores de Layout:

Código: FindFileDialog::FindFileDialog(QWidget *parent)

: QDialog(parent)

{

...

QGridLayout *leftLayout = new QGridLayout;

leftLayout->addWidget(namedLabel, 0, 0);

leftLayout->addWidget(namedLineEdit, 0, 1);

leftLayout->addWidget(lookInLabel, 1, 0);

leftLayout->addWidget(lookInLineEdit, 1, 1);

leftLayout->addWidget(subfoldersCheckBox, 2, 0, 1, 2);

Page 6: Cap6

leftLayout->addWidget(tableWidget, 3, 0, 1, 2);

leftLayout->addWidget(messageLabel, 4, 0, 1, 2);

QVBoxLayout *rightLayout = new QVBoxLayout;

rightLayout->addWidget(findButton);

rightLayout->addWidget(stopButton);

rightLayout->addWidget(closeButton);

rightLayout->addStretch();

rightLayout->addWidget(helpButton);

QHBoxLayout *mainLayout = new QHBoxLayout;

mainLayout->addLayout(leftLayout);

mainLayout->addLayout(rightLayout);

setLayout(mainLayout);

setWindowTitle(tr("Find Files or Folders"));

}

O layout é controlado por um QHBoxLayout, um QGridLayout, e um QVBoxLayout. O QGridLayout

na esquerda e o QVBoxLayout na direita são colocados lado a lado por outro QHBoxLayout.

A margem em volta da caixa e o espaço entre as widgets crianças são ajustados

com valores default baseados no estilo atual do widget; podem ser trocados usando QLayout::setContentsMargins() e QLayout::setSpacing().

Figura 6.3. O layout da caixa de Pesquisa de Arquivo

A mesma caixa poderia ter sido criada visualmente utilizando o Qt Designer através

da inclusão dos widgets filhos nas posições aproximadas; selecionando aqueles que

devem ser ajustados junto; e clicando em Form|Lay Out Horizontally, Form|Lay Out

Vertically, ou Form|Lay Out in a Grid. Usamos esta abordagem no Capítulo 2 para criar as dialogs Go to Cell e Sort da aplicação Spreadsheet.

Usar QHBoxLayout e QVBoxLayout é um método direto, mas usar QGridLayout requer um

pouco mais de trabalho. QGridLayout trabalha em uma grade bidimensional de células.

O QLabel o canto superior esquerdo do layout está na posição (0,0), e o QLineEdit

Page 7: Cap6

correspondente está na posição (0,1). O QCheckBox abrangem duas colunas; ocupa

as células das posições (2,0) e (2,1). O QTreeWidget e o QLabel abaixo dele também

expande duas colunas. As chamadas a QGridLayout::addWidget() têm a seguinte sintaxe:

layout->addWidget(widget, row, column, rowSpan, columnSpan);

Aqui, widget é o widget-filho para inserir dentro do layout, (row, column) é a célula no

canto superior esquerdo ocupado pelo widget, rowSpan é o número de linhas

ocupadas pelo widget, e columnSpan é o número de colunas ocupadas pelo widget,

Caso sejam omitidos, os argumentos rowSpan e columnSpan assumem valor 1.

A chamada addStretch() diz ao gerenciador vertical do layout para consumir espaço

naquele ponto do layout. Ao adicionar um trecho de um item, mandamos um aviso

ao gerenciador para que coloque qualquer espaço em excesso entre os botões

Close e Help. No Qt Designer, podemos atingir o mesmo efeito inserindo um espaçador. Espaçadores aparecem no Qt Designer como “springs” azuis.

O uso de gerenciadores de layout garante benefícios adicionais àqueles que

discutimos até então. Se adicionarmos um widget em um layout ou removermos

um widget de um layout, o layout irá se adaptar automaticamente à nova situação. O mesmo se aplica se chamarmos hide() ou show() em um widget filho. Caso o

tamanho sugerido do widget filho mude, o layout será refeito automaticamente, levando em conta a nova sugestão de tamanho. Além disso, gerenciadores de

layout automaticamente ajustam um valor mínimo para o form como um todo,

baseado nos tamanhos mínimos e sugestões de tamanho dos widgets filhos do

form.

Nos exemplos apresentados até agora, nós simplesmente colocamos widgets em

layouts e usamos separadores para consumir qualquer excesso de espaço. Em

alguns casos, isto não é suficiente para deixar o layout exatamente do jeito que

desejamos. Nessas situações, podemos ajustar o layout mudando as políticas e sugestões de tamanho dos widgets que estão sendo usados.

Uma política de tamanho do widget diz ao sistema de layout como deve ser

estendido ou encolhido. Qt fornece políticas de padrões de tamanho sensíveis para todos os widgets projetados, porém já que nenhum padrão único pode constar em

cada possível layout, ainda é muito comum entre os desenvolvedores a prática de mudar as regras de tamanhos para um ou outro widget em um form. QSizePolicy

possui componentes vertical e horizontal. Eis os valores mais importantes:

Fixed significa que o widget nunca poderá ser expandido ou encolhido.

Permanecerá sempre do tamanho designado pela sugestão.

Minimum significa que a sugestão de tamanho do widget é o seu tamanho

mínimo. O widget nunca poderá encolher para um valor menor do que a

sugestão, mas pode aumentar de tamanho.

Maximum significa que a sugestão de tamanho do widget é seu tamanho

máximo, podendo-se diminuir o widget para a menor sugestão de tamanho.

Preferred significa que o tamanho sugerido é o tamanho mais indicado, mas é

livre para aumentar ou diminuir tamanho.

Page 8: Cap6

Expanding significa que o widget pode aumentar ou diminuir, mas que sua

tendência é crescer.

A Figura 6.4 resume os significados das diferentes políticas de tamanho, usando um QLabel.

Figura 6.4 O significado de diferentes políticas de tamanho

Na figura, Preferred e Expanding são descritos da mesma forma. Então qual é a

diferença? Quando um form que contém widgets do tipo Preferred e Expanding é

redimensionado, um espaço extra é dado aos widgets marcados com Expanding,

enquanto que os widgets Preferred mantém-se em sua sugestão de tamanho.

Existem duas outras políticas de tamanho: MinimumExpanding e Ignored. O primeiro foi

necessário em pouquíssimos casos em versões mais antigas de Qt, mas não é mais útil; a estratégia mais indicada é usar Expanding e reimplementar minimumSizeHint()

apropriadamente. O último é similar a Expanding, Exceto pelo fato de que ignora a

sugestão de tamanho do widget e a sugestão de tamanho mínimo.

Adicionalmente aos componentes horizontais e verticais das políticas de tamanho, a classe QSizePolicy armazena um fator horizontal e vertical para alargamentos de

fatores. Esses fatores de alargamentos podem ser usados para indicar que

diferentes widgets filhos devem crescer em taxas diferentes quando o form expande. Por exemplo, se tivermos um QTreeWidget sobre um QTextEdit e queremos que

QTextEdit seja o dobro do tamanho de QTreeWidget, podemos ajustar o fator de

alargamento vertical de QTextEdit para 2 e o fator de alargamento vertical de

QTreeWidget para 1.

Page 9: Cap6

Layouts Empilhados

A classe QStackedLayout projeta um conjunto de widgets filhos, ou “páginas”, e

mostre um de cada vez, escondendo os demais do usuário. QStackedLayout em si é

invisível e não fornece nenhuma maneira do usuário mudar a págna. As pequenas

flehas e o frame cinza-escuro na Figura 6.5 são fornecidos por Qt Designer para

tornar o layout mais fásil de se modelar. Por conveniência, Qt também inclui

QStackedWidget, que possui um QWidget com um QStackedLayout pré-produzido.

Figura 6.5. QStackedLayout

As páginas são numeradas a partir de 0. Para tornar um widget filho específico

visível, podemos chamar setCurrentIndex() com um número de página. Para obter

o número de página para um widget filho, utilize indexOf().

A caixa de Preferências mostrada na Figura 6.6 é um exemplo de uso de

QStackedLayout. A caixa consiste de um QListWidget na esquerda, e um

QStackedLayout na direita. Cada item em QListWidget corresponde a uma página

diferente no QStackedLayout. Eis o código relevante do construtor da caixa:

PreferenceDialog::PreferenceDialog(QWidget *parent)

: QDialog(parent)

{

...

listWidget = new QListWidget;

listWidget->addItem(tr("Appearance"));

listWidget->addItem(tr("Web Browser"));

listWidget->addItem(tr("Mail & News"));

listWidget->addItem(tr("Advanced"));

stackedLayout = new QStackedLayout;

stackedLayout->addWidget(appearancePage);

Page 10: Cap6

stackedLayout->addWidget(webBrowserPage);

stackedLayout->addWidget(mailAndNewsPage);

stackedLayout->addWidget(advancedPage);

connect(listWidget, SIGNAL(currentRowChanged(int)),

stackedLayout, SLOT(setCurrentIndex(int)));

...

listWidget->setCurrentRow(0);

}

Figura 6.6 Duas páginas da caixa Preferências

Criamos um QListWidget e o populamos com os nomes de páginas. Depois, criamos

um QSTackedLayout e chamamos addWidget() para cada página. Conectamos o

sinal de currentRowChanged(int) de cada widget da lista com setCurrentIndex(int)

do layout empilhado para implementar a troca de páginas e chamamos

setCurrentRow() na lista de widget no final do construtor para começar na página

0.

Formas como esta são muito fáceis de se criar usando o Qt Designer:

1. Crie um novo form beaseado em um dos templates “Dialog”, ou no

template “Widget”.

2. Adicione um QListWidget e um QStackedWidget no form.

3. Preencha cada página com widgets filhos e layout.

(Para criar uma nova página, clique com o botão direito e selecione

Insert Page; para trocar páginas, clique na pequena seta “esquerda ou direita” ,

localizadas no canto superior direito de QStackedWidget.)

4. Modele os widgets lado a lado usando um layout horizontal.

5. Conecte o sinal de currentRowChanged(int) do widget da lista com o

slot de setCurrentIndex(int) do widget empilhado.

Page 11: Cap6

6. Ajuste o valor de currentRow do widget da lista para 0.

Já que implementamos a troca de páginas usando sinais e slots pré-definidos, a

caixa irá exibir o comportamento correto quando pré-visualizada no Qt Designer.

Para casos onde o número de páginas é pequeno e propenso a permanescer

pequeno, uma alternativa mais simples para o uso de QStackedWidget e

QListWidget é o uso de um QTabWdget.

Page 12: Cap6

Separadores

Um QSplitter é um widget que contém outros widgets. Os widgets em um

separador (splitter) são separados por alças separadoras. Usuários podem mudar

os tamanhos dos widgets filhos de um separador , arrastando as alças.

Separadores podem ser usados como uma alternativa para gerenciadores de

layout, para dar mais controle ao usuário.

Os widgets filhos de um QSplitter são automaticamente posicionados lado a lado

(ou um abaixo do outro) na ordem em que foram criados, como barras separadoras

entre widgets adjacentes. Aqui está o código para criação da janela da Figura 6.7:

int main(int argc, char *argv[])

{

QApplication app(argc, argv);

QTextEdit *editor1 = new QTextEdit;

QTextEdit *editor2 = new QTextEdit;

QTextEdit *editor3 = new QTextEdit;

QSplitter splitter(Qt::Horizontal);

splitter.addWidget(editor1);

splitter.addWidget(editor2);

splitter.addWidget(editor3);

...

splitter.show();

return app.exec();

}

Figura 6.7. Aplicação Splitter

O exemplo consiste de três campos QTextEdit, modelados horizontalmente por um

widget QSplitter – isto é mostrado esquematicamente na Figura 6.8. Diferente dos

gerenciadores de layout, que simplesmente modelam os widgets filhos de um form

e não possuem representação visual, QSplitter é derivado de QWidget e pode ser

usado como qualquer outro widget.

Page 13: Cap6

Figura 6.8. Os widgets da aplicação Splitter

Áreas de Rolagem

A classe QScrollArea fornece uma interface de rolagem e duas barras de rolagem.

Se quisermos adicionar barras de rolagem em um widget, é mais simples usar

QScrollArea do que instanciar todos os QSCrollBar e implementar as

funcionalidades de rolagem.

A maneira correta de se usar QScrollArea é através da chamda a setWidget() com o

widget no qual adicionaremos barras de rolagem. QScrollArea automaticamente

ajusta o widget para que este se torne um filho da janela principal (acessível

através de QScrollArea::viewport()), caso este não o seja. Por exemplo, se

quisermos barras de rolagem em volta do widget IconEditor feito no capítulo 5 (

mostrado na Figura 6.11), podemos escrever o seguinte:

int main(int argc, char *argv[])

{

QApplication app(argc, argv);

IconEditor *iconEditor = new IconEditor;

iconEditor->setIconImage(QImage(":/images/mouse.png"));

QScrollArea scrollArea;

scrollArea.setWidget(iconEditor);

scrollArea.viewport()->setBackgroundRole(QPalette::Dark);

scrollArea.viewport()->setAutoFillBackground(true);

scrollArea.setWindowTitle(QObject::tr("Icon Editor"));

scrollArea.show();

return app.exec();

}

Figura 6.11. Redimensionando um QScrollArea

Page 14: Cap6

O QScrollArea ( mostrado esquematicamente na Figura 6.12) mostra o widget em

seu tamanho atual ou usa a sugestão de tamanho caso o widget não tenha sido

redimensionado ainda. Através da chamada a setWidgetResizable(true), podemos

dizer a QScrollArea para automaticamente redimensionar o widget para tomar

vantagem de qualquer espaço extra além do seu tamanho sugerido.

Figura 6.12. Widgets que constituem QScrollArea

Por padrão, as barras de rolagem são exibidas somente quando a janela de

visualização é menor do que o widget filho. Podemos forçar as barras de rolagem a

sempre serem mostradas, através do ajuste a alguns controles de rolagem:

scrollArea.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

scrollArea.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

QScrollArea herda muito de suas funcionalidades de QAbstractScrollArea. Classes

como QTextEdit e QAbstractItemView ( a base das classes de visualização do Qt)

derivam de QAbstractScrollArea, assim não precisamos envolvê-las em um

QScrollArea para adquirir barras de rolagem.

Page 15: Cap6

Janelas e Barras de Ferramentas Anexáveis

Janelas anexadas são janelas que podem ser inseridas dentro de um QMainWindow

ou soltas como janelas independentes. QMainWindow fornece quatro áreas para

janelas anexadas: uma acima, uma á esquerda, uma abaixo, e uma à direita do

widget central. Aplicações como Microsoft Visual Studio e Qt Linguist fazem uso de

janelas anexadas para criar uma interface de usuário mais flexível. Em Qt, janelas

anexadas são instâncias de QDockWidget. A Figure 6.13 mostra uma aplicação Qt

com barras de ferramentas e uma janela anexada.

Figura 6.13. Uma QMainWindow com uma janela anexada

Cada janela anexada tem sua própria barra de título, mesmo quando está anexada.

Usuários podem mover janelas anexadas de um ponto de anexação para outro,

arrastando a barra de título. Podem inclusive despregar uma janela anexada de sua

área, e deixar a janela flutuar como uma janela independente, arrastando a janela

para fora de qualquer área de anexação. Janelas livres estão sempre “on top” sobre

a janela principal. Usuários podem fechar uma QDockWidget clicando no botão

fechar na barra de título da janela. Qualquer combinação dessas funcionalidades

pode ser desabilitada através de uma chamada a QDockWidget::setFeatures().

Em versões anteriores do Qt, barras de ferramentas eram tratadas como janelas

anexadas compartilhavam as mesmasáreas de anexação. A partir de Qt 4, barras

de ferramentas passaram a ocupar suas próprias áreas em volta do widget

central(como mostra a Figura 6.14) e não podem ser desanexadas. Se é necessária

Page 16: Cap6

uma barra flutuantes, podemos simplesmente a colocar dentro de um

QDockWidget.

Figura 6.14. Áreas da barra de ferramentas e anexação de QMainWindow

As curvas indicadas com linhas pontilhadas podem pertencer às qualquer uma das

duas áreas de anexação contíguas. Por exemplo, poderíamos fazer a curva no canto

esquerdo superior pertencer à área de anexação esquerda, chamando

QMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea).

O código que segue mostra como envolver um widget existente (neste caso, um

QTreeWidget) em um QDockWidget e inseri-lo na região de anexação direita:

QDockWidget *shapesDockWidget = new QDockWidget(tr("Shapes"));

shapesDockWidget->setObjectName("shapesDockWidget");

shapesDockWidget->setWidget(treeWidget);

shapesDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea

| Qt::RightDockWidgetArea);

addDockWidget(Qt::RightDockWidgetArea, shapesDockWidget);

Page 17: Cap6

A chamada a setAllowedAreas() especifica restrições nas quais área de anexação

podem aceitar a janela anexada. Aqui, apenas permitimos o usuário a arrastar a

janela de anexação dentro das áreas de anexação esquerda e direita, onde existe

espaço vertical suficiente para que seja exibido corretamente. Se nenhuma área

permitida for setada explicitamente, o usuário pode arrastar a janela para qualquer

uma das quatro áreas.

Todo QObject pode receber um “nome de objeto”. Este nome pode ser útil na hora

do debug e é usado por algumas ferramentas de teste. Normalmente não nos

importamos em dar nomes de objeto aos widgets, mas quando criamos janelas e

barras anexadas, devemos nomear os objetos caso queiramos usar

QMainWindow::saveState() e QMainWindow::restoreState() para salvar e restaurar

os aspectos geométricos e estados da janela anexada e da barra de ferramentas

anexada.

Aqui está o código de criação de barra de ferramentas contendo um QComboBox,

um QSpinBox, e alguns QToolButton’s de um construtor da subclasse de

QMainWindow:

QToolBar *fontToolBar = new QToolBar(tr("Font"));

fontToolBar->setObjectName("fontToolBar");

fontToolBar->addWidget(familyComboBox);

fontToolBar->addWidget(sizeSpinBox);

fontToolBar->addAction(boldAction);

fontToolBar->addAction(italicAction);

fontToolBar->addAction(underlineAction);

fontToolBar->setAllowedAreas(Qt::TopToolBarArea

| Qt::BottomToolBarArea);

addToolBar(fontToolBar);

Se quisermos salvar a posição de todas as janelas e barras de ferramentas

anexáveis, a fim de ser possível restaurá-las na próxima vez que a aplicação for

executada, podemos usitlizar um código que é similar ao código usado para salvar

o estado de um QSplitter, usando as funções saveState() e restoreState() de

QMainWindow:

void MainWindow::writeSettings()

{

QSettings settings("Software Inc.", "Icon Editor");

settings.beginGroup("mainWindow");

settings.setValue("geometry", saveGeometry());

settings.setValue("state", saveState());

settings.endGroup();

}

void MainWindow::readSettings()

{

Page 18: Cap6

QSettings settings("Software Inc.", "Icon Editor");

settings.beginGroup("mainWindow");

restoreGeometry(settings.value("geometry").toByteArray());

restoreState(settings.value("state").toByteArray());

settings.endGroup();

}

Finalmente, QMainWindow fornece um menu de contexto que lista todas as janelas

e baras de ferramentas anexáveis. Este menu é mostrado na Fiura 6.15. O usuário

pode fechar e restaurar janelas anexáveis e esconder e restaurar barras de

ferramentas através do menu.

Figura 6.15. O menu de contexto de QMainWindow

Page 19: Cap6

Interface de Documento Múltiplo

Aplicações que fornecem documentos múltiplos dentro da área central da janela são

as chamadas aplicações de interface de documentos, ou aplicações MDI. Em Qt,

uma aplicação MDI é criada usando a classe QMidArea como o widget central e

fazendo cada janela de documento uma sub-janela QMdiArea.

È convencional para aplicações MDI fornecer um menu Window que inclua alguns

comandos para manusear ambas janelas e a ista de janelas. A janela ativa é

identificada com uma marca. O usuário pode tornar qualquer janela ativa, clicando

em sua entrada no menu WIndow.

Nesta seção, vamos desenvolver a aplicação MDI Editor mostrada na Figura 6.16

para demonstrar como criar uma aplicação MDI e como implementar seu menu

Window. Todos os menus da aplicação são mostrados na Figura 6.17.

Figura 6.16. A Aplicação MDI Editor

Page 20: Cap6

Figura 6.17. Os menus da aplicação MDI Editor

A aplicação consiste de duas classes: MainWindow e Editor. O código é

incrementado com os exemplos do livro, e já que a maioria do que veremos é

similar ou a mesma da aplicação Spreadsheet, vista na Parte 1, apresentaremos

apenas o código MDI-relevante.

Iniciemos com a classe MainWindow.

MainWindow::MainWindow()

{

mdiArea = new QMdiArea;

setCentralWidget(mdiArea);

connect(mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)),

this, SLOT(updateActions()));

createActions();

createMenus();

createToolBars();

createStatusBar();

setWindowIcon(QPixmap(":/images/icon.png"));

setWindowTitle(tr("MDI Editor"));

QTimer::singleShot(0, this, SLOT(loadFiles()));

}

No construtor de MainWindow, criamos um widget QMidArea e o tornamos o widget

central. Conectamos o sinal subWindowActivated() de QMidArea para o slot em

que usaremos para manter o menu da janela atualizado, e no qual nos

asseguramos que as ações estão habilitadas ou desabilitadas dependendo do

estado da aplicação.

No fim do construtor, ajustamos um timer com intervalo de 0 milissegundos para

chamar a função loadFiles(). Esses temporizadores disparam assim que o ciclo

de eventos fica ocioso. Em prática, isto significa que o construtor encerrará, e

depois que a janela principal for mostrada, loadFiles() será chamado. Se não

Page 21: Cap6

fizermos isto existirem muitos arquivos grandes a serem carregados, o construtor

não encerrará enquanto todos os arquivos não forem carregados, e enquanto isto,

o usuário não verá nada na tela e pode pensar que a aplicação falhou ao iniciar.

void MainWindow::loadFiles()

{

QStringList args = QApplication::arguments();

args.removeFirst();

if (!args.isEmpty()) {

foreach (QString arg, args)

openFile(arg);

mdiArea->cascadeSubWindows(); } else {

newFile();

}

mdiArea->activateNextSubWindow();

}

Caso o usuário tenha iniciado a aplicação com um ou mais nomes de arquivos na

linha de comando, esta função tenta carregar cada arquivo e no final cascateia as

sub-janelas de forma que o usuário pode vê-las facilmente. Opções específicas de

linhas de comando do Qt, como –style e –font, são automaticamente removidas

da lista de argumentos pelo construtor QAplication. Assim, se escrevermos

mdieditor -style motif readme.txt

na linha de comando, QAplication::arguments() retorna um QStringList

contendo dois itens (“mdieditor” e “readme.txt”), e a aplicação MDI Editor inicia

com o documento readme.txt.

Se nenhum arquivo for especificado na linha de comando, uma nova sub-janela de

editor vazia é criada para que o usuário possa começar a digitar. A chamada para

activateNextSubWindow() significa que uma janela de edição recebe foco, e

assegura que a função updateActions() é chamada para atualizar o menu Window

e habilitar e desabilitar ações, de acordo com o estado da aplicação.

void MainWindow::newFile()

{

Editor *editor = new Editor;

editor->newFile();

addEditor(editor);

}

O slot newFile() corresponde à opção File|New do menu. Ela cria um widget

Editor e passa ele para a função privada addEditor().

Page 22: Cap6

void MainWindow::open()

{

Editor *editor = Editor::open(this);

if (editor)

addEditor(editor);

}

A função open() corresponde à File|Open. Ela faz uma chamada para a função

estática Editor::open(), que abre uma janela para selecionar arquivo para abrir.

Se o usuário escolhe um arquivo, um novo Editor é criado, o texto do arquivo é

lido, e caso a leitura seja feita com sucesso, o ponteiro para o Editor é retornado.

Caso o usuário cancele a janela de abertura de arquivo, ou se a leitura falhar, um

ponteiro null é retornado e o usuário é notificado do erro. Faz mais sentido

implementar as operações sob arquivos na classe Editor do que na classe

MainWindow, já que cada Editor precisa manter seu próprio estado independente.

void MainWindow::addEditor(Editor *editor)

{

connect(editor, SIGNAL(copyAvailable(bool)),

cutAction, SLOT(setEnabled(bool)));

connect(editor, SIGNAL(copyAvailable(bool)),

copyAction, SLOT(setEnabled(bool)));

QMdiSubWindow *subWindow = mdiArea->addSubWindow(editor);

windowMenu->addAction(editor->windowMenuAction());

windowActionGroup->addAction(editor->windowMenuAction());

subWindow->show();

}

A função privada addEditor() é chamada em newFile() e open() para completar

a inicialização de um novo widget Editor. Ela começa estabelecendo duas conexões

sinal-slot. Essas conexões garantem que Edit|Cut e Edit|Copy sejam habilitadas ou

desabilitadas, dependendo se há texto selecionado.

Como estamos usando MDI, é possível que mútiplos widgets Editor estejam em

uso. Isto é algo para se tomar cuidado, pois estamos interessados apenas em

responder ao sinal copyAvailable(bool) da janela Editor ativa, e não das demais.

Mas estes sinais podem apenas ser emitidos pela janela ativa, então isto deixa de

ser um problema na prática.

A função QMdiArea::addSubWindow() cria um novo QMdiSubWindow, insere o

widget passado como parâmetro dentro da sub-janela, e retorna a sub-janela.

Depois, criamos um QAction que representa a janela para o menu Window. A ação

é fornecida pela classe Editor, que será vista em breve. Também adicionamos a

ação para um objeto QActionGroup. Este garante que apenas um item Window do

menu é checado por vez. Finalmente, chamamos show() na nova QMdiSubWindow

para a tornar visível.

Page 23: Cap6

void MainWindow::save()

{

if (activeEditor())

activeEditor()->save();

}

O slot save() faz uma chamada a Editor::save() no editor ativo, caso haja um.

Novamente, o código que realiza o verdadeiro trabalho é localizado na classe

Editor.

Editor *MainWindow::activeEditor()

{

QMdiSubWindow *subWindow = mdiArea->activeSubWindow();

if (subWindow)

return qobject_cast<Editor *>(subWindow->widget());

return 0;

}

A função privada activeEditor() retorna o widget abrigado dentro da subjanela

ativa como um ponteiro do tipo Editor, ou um ponteiro nulo caso não haja uma

subjanela ativa.

void MainWindow::cut()

{

if (activeEditor())

activeEditor()->cut();

}

O slot cut() chama Editor::cut() no editor ativo. Não mostramos os slots

copy() e paste() pois estes seguem o mesmo padrão.

void MainWindow::updateActions()

{

bool hasEditor = (activeEditor() != 0);

bool hasSelection = activeEditor()

&& activeEditor()>textCursor().hasSelection();

saveAction->setEnabled(hasEditor);

saveAsAction->setEnabled(hasEditor);

cutAction->setEnabled(hasSelection);

copyAction->setEnabled(hasSelection);

pasteAction->setEnabled(hasEditor);

closeAction->setEnabled(hasEditor);

closeAllAction->setEnabled(hasEditor);

tileAction->setEnabled(hasEditor);

Page 24: Cap6

cascadeAction->setEnabled(hasEditor);

nextAction->setEnabled(hasEditor);

previousAction->setEnabled(hasEditor);

separatorAction->setVisible(hasEditor);

if (activeEditor())

activeEditor()->windowMenuAction()->setChecked(true);

}

O sinal subWindowActivated() é emitido toda vez que uma nova subjanela se

torna ativa, e quando a última subjanela é fechada (no caso, seu parâmetro é um

ponteiro para null). O sinal é conectado ao slot updateActions().

A maioria das opções dos menus faz sentido apenas se há uma janela ativa, sendo

assim as desabilitamos caso não haja uma janela. No fim, chamamos

setChecked() na QAction que representa a janela ativa. Graças a QActionGroup,

não precisamos desmarcar explicitamente a janela ativa anterior.

void MainWindow::createMenus()

{

...

windowMenu = menuBar()->addMenu(tr("&Window"));

windowMenu->addAction(closeAction);

windowMenu->addAction(closeAllAction);

windowMenu->addSeparator();

windowMenu->addAction(tileAction);

windowMenu->addAction(cascadeAction);

windowMenu->addSeparator();

windowMenu->addAction(nextAction);

windowMenu->addAction(previousAction);

windowMenu->addAction(separatorAction);

...

}

A função privada createMenus() preenche o menu Window com ações. Todas as

ações são típicas de menus e são facilmente implementadas usando os slots de

QMdiArea,closeActiveSubWindow(), closeAllSubWindows(), tileSubWindows(),

e cascadeSubWIndows(). Toda vez que o usuário abre uma nova janela, ela é

adicionada à lista de ações do menu Window. (Isto é feito na função addEditor()

que vimos na página 160). Quando o usuário fecha uma janela de edição, sua ação

no meno Window é deletada (já que a ação respectiva é propriedade da janela de

edição), assim a ação é automaticamente removida do menu Window.

void MainWindow::closeEvent(QCloseEvent *event)

{

mdiArea->closeAllSubWindows();

if (!mdiArea->subWindowList().isEmpty()) {

Page 25: Cap6

event->ignore();

} else {

event->accept();

}

}

A função closeEvent() é reimplementada para fechar todas as subjanelas, fazendo

com que cada subjanela receba um evento de fecho. Se uma subjanela “ignorar”

seu evento de fecho ( caso o usuário cancele uma caixa de mensagens “ unsaved

changes”), ignoramos o evento de fecho para MainWindow; de outra forma,

aceitamos este, fazendo com que o Qt encerre a aplicação por completo. Se não

tivéssemos reimplementado closeEvent() em MainWindow, não seria dada ao

usuário oportunidade de salvar mudanças não salvas.

Terminamos agora nossa revisão de MainWindow, assim podemos prosseguir na

implementação do Editor. A classe Editor representa uma subjanela. É derivada de

QTextEdit, que fornece a funcionalidade de edição de texto. Em uma aplicação do

mundo real, se um componente de edição de código é exigido, podemos considerar

usar o Scintilla, disponível para Qt como QScintilla em

HTTP://www.riverbankcomputing.uk/qscintilla/.

Assim como qualquer widget Qt pode ser usado como uma janela independente,

qualquer widget Qt pode ser inserido dentro de um QMdiSubWindow e usado como

uma subjanela em uma área MDI.

Aqui está a definição da classe:

class Editor : public QTextEdit

{

Q_OBJECT

public:

Editor(QWidget *parent = 0);

void newFile();

bool save();

bool saveAs();

QSize sizeHint() const;

QAction *windowMenuAction() const { return action; }

static Editor *open(QWidget *parent = 0);

static Editor *openFile(const QString &fileName,

QWidget *parent = 0);

protected:

void closeEvent(QCloseEvent *event);

private slots:

void documentWasModified();

private:

bool okToContinue();

Page 26: Cap6

bool saveFile(const QString &fileName);

void setCurrentFile(const QString &fileName);

bool readFile(const QString &fileName);

bool writeFile(const QString &fileName);

QString strippedName(const QString &fullFileName);

QString curFile;

bool isUntitled;

QAction *action;

};

Quatro das funções privadas que estavam na classe MainWindow da aplicação

Spreadsheet estão também presentes na classe Editor: okToContinue(),

saveFile(), setCurrentFile(), e strippedName().

Editor::Editor(QWidget *parent)

: QTextEdit(parent)

{

action = new QAction(this);

action->setCheckable(true);

connect(action, SIGNAL(triggered()), this, SLOT(show()));

connect(action, SIGNAL(triggered()), this, SLOT(setFocus()));

isUntitled = true;

connect(document(), SIGNAL(contentsChanged()),

this, SLOT(documentWasModified()));

setWindowIcon(QPixmap(":/images/document.png"));

setWindowTitle("[*]");

setAttribute(Qt::WA_DeleteOnClose);

}

Primeiro, criamos uma QAction que representa o editor no menu Window da

aplicação, e conectamos essa ação aos slots show() e setFocus().

Já que permitimos usuários a criar qualquer número de janelas de edição, devemos

fazer algumas disposições para nomeá-las para que elas possam ser distinguidas

antes de serem salvas pela primeira vez. Uma forma comum de controlar isto é

alocando nomes que incluam números (ex: document1.txt). Usamos a variável

isUntitled para distinguir entre nomes fornecidos pelo usuário e nomes que o

programa gera automaticamente.

Conectamos o sinal contentsChanged do documento de texto para o slot privado

documentWasModified(). Este slot faz uma chamada para

setWindowModified(true).

Finalmente, setamos o atributo Qt::WA_DeleteOnClose para evitar vazamentos de

memória quando o usuário fechar uma janela Editor.

Page 27: Cap6

void Editor::newFile()

{

static int documentNumber = 1;

curFile = tr("document%1.txt").arg(documentNumber);

setWindowTitle(curFile + "[*]");

action->setText(curFile);

isUntitled = true;

++documentNumber;

}

A função newFile() gera um nome como document1.txt para o novo documento.

O código pertence a newFile(), e não ao construtor, pois não queremos consumir

números quando chamamos open() para abrir um documento existente em uma

nova janela Editor criada. Já que documentNumber é declarado estático, ele é

dividido através de todas as instancias de Editor.

O marcador “[*]” no título da janela é um marcador de local para quando

quisermos que o asterisco apareça quando o arquivo possuir mudanças não-salvas

em plataformas que não sejam Mac OS X. Para Mac, documentos não salvos

possuem um ponto no botão de fechar da janela. Cobrimos este marcador de local

no Capítulo 3 (p.61).

Além de criar novos arquivos, usuários freqüentemente querem abrir arquivos

existentes, carregados ou de uma caixa de diálogo de arquivo, ou uma lista de

arquivos recentemente abertos. Duas funções estáticas São fornecidas para

suportar esses usos: open() para escolher um nome de arquivo do sistema de

arquivos, e openFile() para criar um Editor e ler o conteúdo de um arquivo

específico.

Editor *Editor::open(QWidget *parent)

{

QString fileName =

QFileDialog::getOpenFileName(parent, tr("Open"), ".");

if (fileName.isEmpty())

return 0;

return openFile(fileName, parent);

}

A função estática open() faz surgir uma caixa de diálogo de arquivo, pela qual o

usuário pode escolher um arquivo. Se um arquivo é escolhido, openFile() é

chamado para criar um Editor e lê o conteúdo do arquivo.

Editor *Editor::openFile(const QString &fileName, QWidget *parent)

{

Editor *editor = new Editor(parent);

if (editor->readFile(fileName)) {

editor->setCurrentFile(fileName);

Page 28: Cap6

return editor;

} else {

delete editor;

return 0; }

}

A função estática começa criando um novo widget Editor, e depois tenta ler no

arquivo específico. Caso a leitura seja feita com sucesso, o Editor é retornado;

caso contrário, o usuário é informado do problema (em readFile()), o editor é

deletado, e um ponteiro para null é retornado.

bool Editor::save()

{

if (isUntitled) {

return saveAs();

} else {

return saveFile(curFile);

}

}

A função save() usa a variável isUntitled para determinar se ela deve chamar

saveFile() ou saveAs().

void Editor::closeEvent(QCloseEvent *event)

{

if (okToContinue()) {

event->accept();

} else {

event->ignore();

}

}

A função closeEvent() é reimplementada para permitir o usuário a salvar

mudanças não-salvas. A lógica é codificada na função okToContinue(), que faz

gera um pop up de uma caixa de mensagem que pergunta, “Do you want to save

your changes?” se okToContinue() retornar true, aceitamos o evento de fecho; de

outra forma, “ignoramos” a mensagem e deixamos a janela inalterada.

void Editor::setCurrentFile(const QString &fileName)

{

curFile = fileName;

isUntitled = false;

action->setText(strippedName(curFile));

document()->setModified(false);

setWindowTitle(strippedName(curFile) + "[*]");

setWindowModified(false);

}

Page 29: Cap6

A função setCurrentFile() é chamada de openFile() e saveFile() para

atualizar as variáveis curFile e isUntitled, para habilitar o título da janela e

texto de ação, e para habilitar o flag “modified” do documento para false. A

Qualquer momento em que o usuário modifica o texto no editor, o subjacente

QTextDocument emite o sinal contentsChanged() e habilita sua flag interna

“modified” para true.

QSize Editor::sizeHint() const

{

return QSize(72 * fontMetrics().width('x'),

25 * fontMetrics().lineSpacing());

}

Finalmente, a função sizeHint() retorna um tamanho baseado no tamanho da

letra “x” e na altura da letra de uma linha de texto. QMdiArea usa a sugestão de

tamanho para dar um tamanho inicial à janela.

MDI é uma forma de controlas múltiplos documentos simultaneamente. Em Max OS

X, a abordagem preferida é usar janelas top-level mútliplas. Cobrimos esta

abordagem na seção “Documentos Múltiplos” do Capítulo 3.