10
9 . Arrastar e Soltar Habilitando Arrastar e Soltar Suporte a tipos Customizados de Arrastamento Controlando a Área de Transferência “Drag and Drop”, ou Arrastar e soltar é uma forma moderna e intuitiva de transferir informação em uma aplicação ou entre aplicações diferentes. È geralmente fornecida em adição ao suporte a clipboard para mover e copiar dados. Neste capítulo, veremos como adicionar suporte a arrastar e soltar a uma aplicação e como controlar formatos customizados. Então mostraremos como reusar o código do arrastar e soltar para adicionar suporte ao clipboard. Esta reutilização de código é possível porque ambos mecanismos são baseados em QMimeData, uma classe que pode fornecer dados em diversos formatos. Habilitando Arrastar e Soltar Drag e Drop envolve duas ações distintas: Arrastar e soltar algo. Widgets do Qt podem servir como pontos de arrastamento, ou como pontos de soltura, ou como ambos. Nosso primeiro exemplo mostra como fazer uma aplicação Qt aceitar um arrastamento iniciado por outra aplicação. A aplicação Qt é uma janela principal como um QTextEdit como seu widget central. Quando o usuário carrega um arquivo texto da Área de Trabalho ou por um explorador de arquivos e o solta na aplicação, a aplicação carrega o arquivo dentro de QTextEdit. Aqui está a definição da classe MainWindow do exemplo: Class MainWindow : public QMainWindow { Q_OBJECT; public: MainWindow(); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event);

Cap9

Embed Size (px)

Citation preview

Page 1: Cap9

9 . Arrastar e Soltar

Habilitando Arrastar e Soltar

Suporte a tipos Customizados de Arrastamento

Controlando a Área de Transferência

“Drag and Drop”, ou Arrastar e soltar é uma forma moderna e intuitiva de transferir informação em uma aplicação ou

entre aplicações diferentes. È geralmente fornecida em adição ao suporte a clipboard para mover e copiar dados.

Neste capítulo, veremos como adicionar suporte a arrastar e soltar a uma aplicação e como controlar formatos

customizados. Então mostraremos como reusar o código do arrastar e soltar para adicionar suporte ao clipboard.

Esta reutilização de código é possível porque ambos mecanismos são baseados em QMimeData, uma classe que

pode fornecer dados em diversos formatos.

Habilitando Arrastar e Soltar

Drag e Drop envolve duas ações distintas: Arrastar e soltar algo. Widgets do Qt podem servir como pontos de

arrastamento, ou como pontos de soltura, ou como ambos.

Nosso primeiro exemplo mostra como fazer uma aplicação Qt aceitar um arrastamento iniciado por outra aplicação.

A aplicação Qt é uma janela principal como um QTextEdit como seu widget central. Quando o usuário carrega

um arquivo texto da Área de Trabalho ou por um explorador de arquivos e o solta na aplicação, a aplicação carrega o

arquivo dentro de QTextEdit.

Aqui está a definição da classe MainWindow do exemplo:

Class MainWindow : public QMainWindow

{

Q_OBJECT;

public:

MainWindow();

protected:

void dragEnterEvent(QDragEnterEvent *event);

void dropEvent(QDropEvent *event);

Page 2: Cap9

private:

bool readFile(const QString &fileName);

QTextEdit *textEdit;

};

A classe MainWindow reimplementa dragEnterEvemt() e dropEvent() de QWidget. Já que o

propósito desse exemplo é mostrar a funcionalidade arrastar e soltar, a maioria da funcionalidade que esperaríamos

ver em uma classe de janela principal foi omitida.

MainWindow::MainWindow()

{

textEdit = new QTextEdit;

setCentralWidget (textEdit);

textEdit->setAcceptDrops(false);

setAcceptDrops(true);

setWindowTitle(tr(“Text Editor”));

}

No construtor, criamos um QTextEdit e o setamos como o widget central. Por padrão, QTextEdit aceita

arrastes textuais de outras aplicações, e se o usuário solta um arquivo dentro dele, o nome do arquivo será inserido

no texto. Já que eventos de drop são propagados do filho para o pai, desabilitando a funcionalidade de soltar no

QTextEdit e a habilitando na janela principal obtemos os eventos de drop para a janela inteira em

MainWindow.

void MainWindow::dragEnterEvent (QDragEnterEvent *event)

{

if(event->mimeData()->hasFormat(“text/uri-list”))

event->acceptProposedAction();

}

O dragEnterEvent()é chamado a qualquer momento em que o usuário arrasta um objeto para dentro do

widget. Se chamarmos acceptProposedAction() no evento, indicamos que o usuário pode soltar o objeto

carregado no widget. Por padrão, o widget não aceitaria o objeto. Qt automaticamente muda o cursor para indicar ao

usuário quando o widget é considerado um ponto de soltura.

Aqui queremos que o usuário seja autorizado a carregar arquivos, mas nada mais. Para tal, checamos o tipo MIME

do carregamento. O tipo MIME text/uri-list é usado para armazenar uma lista de identificadores recursos

uniformes (URIs), os quais podem ser nomes de arquivos, URLs (como caminhos HTTP ou FTP), ou outros

identificadores de recursos globais. Tipos padrão MIME são definidos pela Autoridade de Números Atribuídos da

Internet (IANA). Eles consistem de um tipo e subtipo separados por uma barra. A área de transferência e o sistema

de carregar e soltar usam tipos MIME para identificar diferentes tipos de data. A lista oficial de tipos MIME está

disponível em http://www.iana.org/assignments/media-types/.

void MainWindow::dropEvent(QDropEvent *event)

{

QList<QUrl> urls = event->mimeData()->urls();

if(urls.isEmpty())

return;

QString filename = urls.first().toLocalFile();

if(filename.isEmpty())

return;

if(readFile(fileName))

setWindowTitle(tr(“%1 - %2”).arg(fileName)

.arg(tr(“Drag File”)));

}

Page 3: Cap9

O dropEvent() é chamado quando o usuário solta um objeto no widget. Chamamos

QMimeData::urls() para obter uma lista de QUrls. Tipicamente, usuários arrastam apenas um arquivo de

cada vez, mas é possível arrastar múltiplos arquivos, arrastando uma seleção. Se há mais de uma URL, ou se a URL

não é um nome de arquivo local, retornamos imediatamente.

QWidget também fornece dragMoveEvent() e dragLeaveEvent(), mas para a maioria das

aplicações eles não precisam ser reimplementados.

O segundo exemplo ilustra como iniciar um drag e aceitar um drop. Vamos criar uma subclasse QListWidget

que suporta carregar e soltar, e usá-la como um componente na aplicação Project Chooser mostrada na figura 9.1.

Figura 9.1. A Aplicação Project Chooser

A Aplicação Project Chooser apresenta ao usuário dois widgets lista, populados com nomes. Cada widget lista

representa um objeto. O usuário pode arrastar e soltar os nomes nos widgets para mover uma pessoa de um projeto

para outro.

Todo o código de carregar e soltar está localizado na subclasse QListWidget. Aqui está a definição da classe:

Class ProjectListWidget : public QListWidget

{

Q_OBJECT;

public:

ProjectListWidget(QWidget *parente = 0);

protected:

void mousePressEvent(QMouseEvent *event);

void mouseMoveEvent (QMouseEvent *event);

void dragEnterEvent(QDragEnterEvent *event);

void dragMoveEvent(QDragMoveEvent *event);

void dropEvent(QDropEvent *event);

private:

void performDrag();

QPoint startPos;

};

A classe ProjectListWidget reimplementa cinco controladores de eventos declarados em QWidget.

Page 4: Cap9

ProjectListWidget::ProjectListWidget(QWidget *parent)

: QListWidget(parent)

{

setAcceptDrops(true);

}

No construtor, habilitamos ação de soltar no widget de lista.

Void ProjectListWidget::mousePressEvent(QMouseEvent *event)

{

if (event->button() == Qt::LeftButton)

startPos = event->pos();

QListWidget::mousePressEvent(event);

}

Quando o usuário pressiona o botão esquerdo do mouse, armazenamos a posição do mouse na variável privada

startPos. Chamamos a implementação QlistWidget de mousePressEvent() para assegurar que

QListWidget tem a oportunidade de processas eventos de clique como usualmente.

void ProjectListWidget::mouseMoveEvent(QMouseEvent *event)

{

if(event->buttons() & Qt::LeftButton) {

int distance = (event->pos() - startPos).manhattanLength();

if(distance >= QApplication::startDragDistance())

performDrag();

}

QListWidget::mouseMoveEvent(event);

}

Quando o usuário move o cursor enquanto segura o botão esquerdo do mouse, consideramos o início de um drag.

Computamos a distância entre a posição atual do mouse e a posição onde o botão esquerdo foi pressionado-a

“Manhattan length” é uma aproximação para cálculo rápido do tamanho de um vetor partindo de sua origem. Se a

distância é maior ou igual a distância inicial de arrastamento recomendada pela QApplication (normalmente

quatro pixels), podemos chamar a função privada performDrag() para começar a arrastar. Isto evita iniciar um

drag graças ao movimento da mão do usuário.

void ProjectListWidget::performDrag()

{

QListWidgetItem *item = currentItem();

if (item) {

QMimeData *mimeData = new QMimeData;

mimeData->setText(item->text());

QDrag *drag = new QDrag(this);

drag->setMimeData(mimeData);

drag->setPixmap(QPixmap(“:/images/person.png”));

if (drag->exec(Qt::MoveAction) == Qt::MoveAction)

delete item;

}

}

Em performDrag(), criamos um objeto do tipo QDrag e setamos this como seu pai. O objeto QDrag

armazena os dados em um objeto QMimeData. Para este exemplo, fornecemos os dados no formato string

text/plain usando QMimeData::setText(). QMimeData fornece várias funções para controle da

maioria dos tipos de itens (imagens, URLs, cores, etc) e podem controlar tipos MIME arbitrários representados como

QByteArrays. A chamada para QDrag::setPixmap() seta o ícone que segue o cursor enquanto o objeto

é arrastado.

A chamada QDrag::exec() inicia a operação de arrastamento e bloqueia até o momento em que o usuário

solta a carga arrastada ou cancela o drag. Ela faz uma combinação de chamadas “ações de arrastamento” como

argumento (Qt::CopyAction, Qt::MoveAction, e Qt::LinkAction) e retorna a ação de

Page 5: Cap9

arrastamento que foi executada (ou Qt::IgnoreAction caso nada tenha sido executado). Qual ação é

executada depende do que o widget fonte permite, o que o alvo suporta, e quais teclas modificadoras são

pressionadas quando o item é solto. Depois da chamada a exec(), Qt toma posse do objeto carregado e o deleta

quando ele não é mais necessário.

void ProjectListWidget::dragEnterEvent (QDragEnterEvent *event)

{

ProjectListWidget *source =

qobject_cast<ProjectListWidget *>(event->source());

if (source && source != this) {

event->setDropAction(Qt::MoveAction);

event->accept();

}

}

O widget ProjectListWidget não apenas origina arrastamentos, mas também aceita tais arrastamentos

caso eles venham de outro ProjectListWidget na mesma aplicação.

QDragEnterEvent::source() retorna um ponteiro para o widget que iniciou o arrastamento se o widget

for parte da mesma aplicação; caso contrário, retorna um ponteiro nulo. Podemos usar qobject_cast<T>()

para assegurar que o arrastamento vem de um ProjectListWidget. Se tudo estiver correto, dizemos ao Qt

que estamos prontos para aceitar a ação como uma ação de movimento.

void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event)

{

ProjectListWidget *source =

qobject_cast<ProjectListWidget *>(event->source());

if(source && source != this) {

event->setDropAction (Qt::MoveAction);

event->accept();

}

}

O código em dragMoveEvent() é idêntico ao que fizemos em dragEnterEvent(). É necessário porque

precisamos sobrepor a implementação de QListWidget (na verdade, de QAbstractItemView ) da

função.

void ProjectListWidget::dropEvent(QDropEvent *event)

{

ProjectListWidget *source =

qobject_cast<ProjectListWidget *>(event->source());

if(source && source != this){

addItem(event->mimeData()->text());

event->setDropAction(Qt::MoveAction);

event->accept();

}

}

Em dropEvent(), obtemos o texto arrastado usando QMimeData::text() e criamos um item com esse

texto. Também precisamos aceitar o evento como um “evento de movimentação” para dizer ao widget fonte que ele

pode agora remover a versão original do item arrastado.

Drag and Drop é um mecanismo poderoso para transferência de dados entre aplicações. Mas em alguns casos, é

possível implementar arrastar e soltar sem usar as facilidades de Qt. Se tudo que queremos fazer é mover dados

dentro de um widget em uma aplicação, podemos simplesmente reimplementar mousePressEvent() e

mouseReleaseEvent().

Suporte a Tipos Customizados de Arrastamento

Page 6: Cap9

Nos exemplos até agora, confiamos no suporte de QMimeData para tipos MIME comuns. Além disso, chamamos

QMimeData::setText() para criar um arrastamento de texto, e usamos QMimeData::urls() para

recuperar o conteúdo de um arrastamento text/uri-list. Se quisermos arrastar texto simples, texto HTML,

imagens, URLs ou cores, podemos usar QMimeData sem formalidades. Mas se quisermos arrastar dados

customizados, devemos escolher uma entre as alternativas:

1. Podemos providenciar dados arbitrários como um QByteArray usando

QMimeData::setData() e os extrair mais tarde usando QMimeData::data().

2. Podemos criar uma subclasse de QMimeData e reimplementar formats() e retrieveData()

para controlar nossos tipos customizados de dados.

3. Para operações de arrastar e soltar dentro de uma aplicação, podemos criar uma subclasse QMimeData

e armazenar os dados usando qualquer estrutura de dados que quisermos.

A primeira opção não envolve uso de subclasse, mas possui alguns problemas: Precisamos converter nossa estrutura

de dados para um QByteArray mesmo se o arrastamento não é aceito por último, e se quisermos fornecer

diversos tipos MIME para interagir de uma forma legal com uma grande porção de aplicações, precisamos

armazenar os dados diversas vezes (uma vez para cada tipo MIME). Se os dados forem pesados, isso pode

desacelerar a aplicação desnecessariamente. As segunda e terceira opções podem evitar ou minimizar esses

problemas. Elas nos dão total controle e podem ser usadas em conjunto.

Para mostrar como essas alternativas funcionam, mostraremos como adicionar propriedades de arrastar e soltar em

um QTableWidget. O arrastamento vai fornecer os seguintes tipos de suportes MIME: text/plain,

text/html, e text/csv. Usando a primeira alternativa, iniciar um arrastamento parece assim:

Código:

void MyTableWidget::mouseMoveEvent (QMouseEvent *event)

{

if(event->buttons() & Qt::LeftButton) {

int distance = (event->pos() - startPos).manhattanLength();

if (distance >= QApplication::startDragDistance())

performDrag();

}

QTableWidget::mouseMoveEvent(event);

}

void MyTableWidget::performDrag()

{

QString plaintext = selectionAsPlainText();

if(plaintext.isEmpty())

return;

QMimeData *mimeData = new QMimeData;

mimeData->setText(plainText);

mimeData->setHtml(toHtml(plainText));

mimeData->setData(“text/csv”, toCsv(plainText).toUtf8());

QDrag *drag = new QDrag(this);

drag->setMimeData(mimeData);

if (drag->exec(Qt::CopyAction|Qt::MoceAction)==Qt: :MoveAction)

deleteSelection();

}

A função private performDrag() é chamada de mouseMoveEvent() para iniciar o arrastamento de uma

seleção retangular. Setamos os tipos MIME text/plain e text/html usando setText() e

setHtml(), e setamos o tipo text/csv usando setData(), que toma um tipo MIME arbitrário e um

QByteArray. O código para selectionAsString() é mais ou menos o mesmo de

Spreadsheet::copy() visto no Capítulo 4.

Page 7: Cap9

QString MyTableWidget::toCsv(const QString &plainText)

{

Qstring result = plaintext;

result.replace(“\\”, “\\\\”);

result.replace(“\”, “\\\””);

result.replace(“\t”, “\”\””);

result.replace(“\n”, “\”\n\””);

result.prepend(“\””);

result.append(“\””);

return result;

}

QString MyTableWidget::toHtml (const QString &plainText)

{

QString result = Qt::escape(plainText);

result.replace(“\t”, “<td>”);

result.replace(“\n”,”\n<tr><td>”);

result.prepend(“<table>\n<tr><td>”);

result.append(“\n</table>”);

return result;

}

As funções toCsv() e toHtml() convertem uma string “tabs e novas linhas” para um CSV (valores separados

por vírgula) ou um string HTML. Por exemplo, os dados

Red Green Blue

Cyan Yellow Magenta

são convertidos para

“Red”, “Green”, “Blue”

“Cyan”, “Yellow”, “Magenta”

ou para

<table>

<tr><td>Red<td>Green<Blue>

<tr><td>Cyan<td>Yellow<td>Magenta

</table>

A conversão é feita da maneira mais simples possível, usando QString::replace(). Para exibir caracteres

HTML especiais, podemos usar Qt::escape().

void MyTableWidget::dropEvent(QDropEvent *event)

{

if(event->mimeData()->hasFormat(“text/csv”)) {

QByteArray csvData = event->mimeData()->data(“text/csv”);

QString csvText = QString::fromUtf8(csvData);

event->acceptProposedAction();

} else if(event->mimeData()->hasFormat(“text/plain”)) {

QString plaintext = event->mimeData()->text();

event->acceptProposedAction();

}

}

Embora providenciemos os dados em três formas diferentes, apenas duas delas são aceitas em dropEvent().

Se o usuário arrasta células de uma QTableWidget para um editor HTML, queremos que as células sejam

Page 8: Cap9

convertidas em uma tabela HTML. Mas se o usuário arrasta HTML arbitrário para uma QWidgetTable, não

queremos aceitar.

Para fazer este exemplo funcionar, também precisamos chamar setAcceptDrops(true) e

setSelectionMode(ConsiguousSelection) no construtor MyTableWidget.

Refaçamos agora o exemplo, mas desta vez instanciar a subclasse de QMimeData para postergar ou evitar as

(possivelmente caras) conversões entre QTableWiidgetItems e QByteArray. Aqui está a definição da

nossa subclasse:

class TableMimeData : public QMimeData

{

Q_OBJECT

public:

TableMimeData(const QTableWidget *tableWidget,

Const QTableWidgetSelectionRange *range);

const QTableWidget *tableWidget() const {return myTableWidget; }

QTableWidgetSelectionRange range() const { return myRange; }

QStringList formats() const;

protected:

QVariant retrieveData(const QString &format,

QVariant::Type preferredType) const;

private:

static QString toHtml(const QString &plainText);

static QString toCsv(const QString &plainText);

QString text(int row, int column) const;

QString rangeAsPlainText() const;

Const QTableWidget *myTableWidget;

QTableWIdgetSelectionRange myRange;

QStringList myFormat;

};

Ao invés de armazenas os dados reais, armazenamos um QTableWidgetSelectionRange que especifica

quais células estão sendo arrastadas e mantemos um ponteiro para QTableWidget. As funções formats()

e retrieveData() são reimplementadas de QMimeData.

TableMimeData::TableMimeData(const QTableWidget *tableWidget,

Const QTableWidgetSelectionRange &range)

{

myTableWidget = tableWidget;

myRange = range;

myFormats << ‘text/csv” << “text/html” << “text/plain”;

}

No construtor, inicializamos as variáveis privadas.

QStringList TableMimeData::formats() const

{

Return myFormats;

}

A função formats() retorna uma lista de tipos MIME fornecidos pelo objeto de dados MIME. A ordem precisa

dos formatos é geralmente irrelevante, mas é uma boa prática colocar os “melhores” formatos primeiro. Aplicações

que suportam muitos formatos usarão algumas vezes o primeiro formato que for compatível.

Page 9: Cap9

QStringList TableMimeData::formats()

{

Return myFormats;

}

A função retrieveData() retorna os dados para um dado tipo MIME na forma de QVariant. O valor do

parâmetro de format é normalmente uma das strings retornadas por formats(), mas não podemos assumir

que, já que nem todas as aplicações checam o tipo MIME frente a formats(). As funções text(), html(),

urls(), imageData(), colorData(), e data() fornecidas por QMimeData são implementadas em

termos de retrieveData().

O parâmetro preferredType nos dá uma dica sobre qual tipo devemos inserir na QVariant. Aqui,

ignoramos isto e confiamos a QMimeData a tarefa de converter o valor de retorno no tipo desejado, se for

necessário.

void MyTableWidget::dropEvent(QDropEvent *event)

{

const TableMimeData *tableData =

qobject_cast<const TableMimeData *>(event->mimeData());

if(tableData){

const QTableWidget *otherTable = tableData->tableWidget();

QTableWidgetSelectionRange ptherRange = tableData->range();

event->acceptProposedAction();

} else if (event->mimeData()->hasFormat(“text/csv”)) {

QByteArray csvData = event->mimeData()->data(“text/csv”);

QString csvText = QString::fromUtf8(csvData);

event->acceptProposedAction();

} else if (event->mimeData()->hasFormat(“text/plain”)) {

QString plaintext = event->mimeData()->text();

event->acceptProposedAction();

}

QTableWidget::mouseMoveEvent(event);

}

A função dropEvent() é parecida com a que tivemos mais cedo nesta sessão, mas desta vez a otimizamos,

verificando se podemos converter com segurança o objeto QMimeData em um TableMimeData. Se

qobject_cast<T>() funcionar, significa que o arrastamento foi originado de um MyTableWidget na

mesma aplicação, e podemos diretamente acessar os dados da tabela ao invés de ir através da API de

QMimeData. Se a conversão falhar, extraímos os dados na maneira padrão.

Neste exemplo, codificamos o texto CSV usando a formatação UTF-8. Se quisermos ter certeza de estar usando a

formatação correta, poderíamos usar o parâmetro charset do tipo MIME text/plain para especificar uma formatação

explícita. Aqui estão alguns exemplos:

text/plain;charset=US-ASCII

text/plain;charset=ISSO-8859-1

text/plain;charset=Shift_JIS

text/plain;charset=UTF-8

Controle a Área de Transferência

A maioria das aplicações faz uso do controle de transferência pré-construído do Qt de uma forma ou de outra. Por

exemplo, a classe QTextEdit fornece opções de cut(), copy() e paste() bem como atalhos de teclado,

assim pouco ou nenhum código adicional é necessário.

Page 10: Cap9

Ao escrever nossas próprias classes, podemos acessar a Área de Transferência através de

QApplication::clipboard(), que retorna um ponteiro para o objeto QClipboard da aplicação.

Controlar a área de transferência do sistema é fácil: faça uma chamada a setText(), setImage(), ou

setPixmap() para inserir dados na área de transferência, e chame text(), image() ou pixmap() para

recuperar dados da área de transferência. Já vimos exemplos de uso da área de transferência na aplicação

Spreadsheet do Capítulo 4.

Para algumas aplicações, a funcionalidade construída pode não ser suficiente. Por exemplo, pode ser que

queiramos fornecer que não é apenas texto ou imagem, ou podemos vir a querer fornecer dados em diversos

formatos para máxima interoperabilidade com outras aplicações. Este caso é muito similar com o que encontramos

mais cedo em arrastar e soltar, e a resposta também é similar: Podemos chamar uma subclasse de QMimeData e

reimplementar alguns funções virtuais.

Caso nossa aplicação suporte drag and drop através de uma subclasse customizada QMimeData, podemos

simplesmente reusar a subclasse QMimeData e a colocar na área de transferência usando a função

setMimeData(). Para recuperar os dados, podemos chamar mimeData() na área de transferência.

Em X11, geralmente é possível colar uma seleção clicando no botão do meio de um mouse de três botões. Isto é

feito usando uma área de transferência “de seleção” separada. Se quiser que seus widgets forneçam este tipo de

área de transferência assim como a padrão, você deve passar QClipboard::Selection como um

argumento adicional para as várias chamadas chamas de área de transferência. Por exemplo, aqui vemos como

reimplementaríamos mouseReleaseEvent() em um editor de texto para fornecer suporte a ação de colar

usando o botão do meio do mouse:

void MyTextEditor::mouseReleaseEvent(QMouseEvent *event)

{

QClipboard *clipboard = QApplication::clipboard();

if (event->button() == Qt::MidButton

&& clipboard->supportsSelection() {

QString text = clipboard->text(QClipboard::Selection);

pasteText(text);

}

}

Em X11, a função supportsSelection() retorna true. Em outras plataformas, retorna false.

Se quisermos ser notificados sempre que o conteúdo da área de transferência muda, podemos conectar o sinal

QClipboard::dataChanged() a uma opção customizada.