1) O documento discute vários métodos de layout de widgets em Qt, incluindo posicionamento absoluto, layout manual e gerenciadores de layout. 2) Gerenciadores de layout como QHBoxLayout e QGridLayout automatizam o layout e garantem uma interface adaptável. 3) Outras classes como QStackedLayout e QSplitter permitem layouts flexíveis e interfaces de múltiplos documentos.
1. 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
2. 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)
{
..
3. 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:
5. 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);
6. 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
7. 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.
8. 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.
9. 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);
10. 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.
11. 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.
12. 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.
13. 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
14. 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.
15. 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
16. 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);
17. 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()
{
18. 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
19. 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
20. 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
21. 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().
22. 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.
23. 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);
24. 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()) {
25. 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();
26. 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.
27. 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);
28. 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);
}
29. 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.