Пример "Fridge Magnets"
Файлы:
Пример "Fridge Magnets" показывает, как предоставить более одного типа MIME-закодированных данных с операцией перетаскивания.
В этом приложении пользователь может поиграться с набором магнитиков на холодильник, используя перетаскивание для образования новых предложений из слов нанесённых на магнитики. Пример состоит из двух классов:
- DragLabel - пользовательский виджет, представляющий один магнитик на холодильник.
- DragWidget предоставляет окно главного приложения.
Сначала мы взглянем на класс DragLabel, затем изучим класс DragWidget.
Определение класса DragLabel
Каждый магнитик на холодильник представляется экземпляром класса DragLabel:
class DragLabel : public QLabel
{
public:
DragLabel(const QString &text, QWidget *parent);
QString labelText() const;
private:
QString m_labelText;
};
Каждый экземпляр этого подкласса QLabel будет использоваться для вывода на экран растрового изображения, сгенерированного из текстовой строки. Поскольку мы не можем сохранить и текст, и растровое изображение в обычной метке, мы объявляем закрытую переменную для сохранения исходного текста, а также определяем дополнительную функцию-член чтобы позволить получить доступ к нему.
Реализация класса DragLabel
В конструкторе DragLabel мы сначала создаёем объект QImage, на котором мы рисуем текст магнитика на холодильник и рамку:
DragLabel::DragLabel(const QString &text, QWidget *parent)
: QLabel(parent)
{
QFontMetrics metric(font());
QSize size = metric.size(Qt::TextSingleLine, text);
QImage image(size.width() + 12, size.height() + 12,
QImage::Format_ARGB32_Premultiplied);
image.fill(qRgba(0, 0, 0, 0));
QFont font;
font.setStyleStrategy(QFont::ForceOutline);
Его размер зависит от текущего размера шрифта, а его формат - QImage::Format_ARGB32_Premultiplied; т.е., изображение сохраняется используя premultiplied 32-битный формат ARGB (0xAARRGGBB).
Затем мы создаём объект шрифта, который использует шрифт приложения по умолчанию, и устанавливаем его стилевую стратегию. Стилевая стратегия сообщает алгоритму сопоставления шрифтов какой будет использоваться тип шрифтов, чтобы найти соответствующее семейство по умолчанию. QFont::ForceOutline заставляет использовать контурные шрифты.
Чтобы отрисовать текст и рамку в изображении, мы используем класс QPainter. QPainter предоставляет высоко оптимизированные методы для выполнения большей части рисования, требующегося ГПИ программ. Он может рисовать всё от линий простых до сложных фигур наподобие секторов и дуг. Он также отрисовывает выравненный текст и растровые изображения.
QLinearGradient gradient(0, 0, 0, image.height()-1);
gradient.setColorAt(0.0, Qt::white);
gradient.setColorAt(0.2, QColor(200, 200, 255));
gradient.setColorAt(0.8, QColor(200, 200, 255));
gradient.setColorAt(1.0, QColor(127, 127, 200));
QPainter painter;
painter.begin(&image);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(gradient);
painter.drawRoundedRect(QRectF(0.5, 0.5, image.width()-1, image.height()-1),
25, 25, Qt::RelativeSize);
painter.setFont(font);
painter.setBrush(Qt::black);
painter.drawText(QRect(QPoint(6, 6), size), Qt::AlignCenter, text);
painter.end();
Рисовальщик может быть активирован передачей в конструктор устройства рисования или используя метод begin(), как сделали мы в этом примере. Метод end() его деактивирует. Обратите внимание на то, что последняя функция вызывается автоматически на разрушение когда рисовальщик активируется своим конструктором. Подсказка отображения QPainter::Antialiasing обеспечивает, что механизм рисования будет сглаживать края примитивов, если это возможно.
Когда рисование выполнено, мы преобразуем наше изображение в растровое используя метод QPixmap'а, fromImage(). Этот метод также принимает необязательный аргумент флагов и преобразует переданное изображение в растровое используя заданные флаги для управления преобразованием (аргумент флагов - побитовое ИЛИ всех Qt::ImageConversionFlags; передача 0 для флагов установит все опции по умолчанию).
setPixmap(QPixmap::fromImage(image));
m_labelText = text;
}
В заключение мы устанавливаем свойство pixmap метки и сохраняем текст метки для последующего использования.
Обратите внимание на то, что установка растрового изображения очищает любое предыдущее содержимое, включая любой текст ранее установленный с использованием QLabel::setText(), а также отключает "быстрые" клавиши виджета-партнёра метки, если он есть.
Определение класса DragWidget
Класс DragWidget унаследован от QWidget, предоставляет поддержку для операций перетаскивания:
class DragWidget : public QWidget
{
public:
DragWidget(QWidget *parent = 0);
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dragMoveEvent(QDragMoveEvent *event);
void dropEvent(QDropEvent *event);
void mousePressEvent(QMouseEvent *event);
};
Чтобы сделать виджет реагирующим на операции перетаскивания, мы просто переопределяем обработчики событий dragEnterEvent(), dragMoveEvent() и dropEvent(), унаследованные от QWidget.
Также мы переопределяем mousePressEvent(), чтобы сделать виджет реагирующим на щелчки кнопками мыши. Это там, где мы напишем код для начала операций перетаскивания.
Реализация класса DragWidget
В конструкторе мы сначала открываем файл, содержащий слова наших магнитиков на холодильник:
DragWidget::DragWidget(QWidget *parent)
: QWidget(parent)
{
QFile dictionaryFile(":/dictionary/words.txt");
dictionaryFile.open(QFile::ReadOnly);
QTextStream inputStream(&dictionaryFile);
QFile - устройство ввода/вывода для чтения и записи текстовых и двоичных файлов и ресурсов, а его также может быть использовано само или в комбинации с QTextStream или QDataStream. Мы выбрали использование класса QTextStream для чтения содержимого файла, предоставляющего удобный интерфейс для чтения и записи текста.
Затем мы создаём магнитики на холодильник. Пока есть данные (метод QTextStream::atEnd() возвращает true если в потоке больше нет данных для чтения), мы читаем по одной строке за раз используя метод QTextStream'а, readLine().
int x = 5;
int y = 5;
while (!inputStream.atEnd()) {
QString word;
inputStream >> word;
if (!word.isEmpty()) {
DragLabel *wordLabel = new DragLabel(word, this);
wordLabel->move(x, y);
wordLabel->show();
wordLabel->setAttribute(Qt::WA_DeleteOnClose);
x += wordLabel->width() + 2;
if (x >= 245) {
x = 5;
y += wordLabel->height() + 2;
}
}
}
Для каждой строки мы создаём объект DragLabel, используя прочитанную строку как текст, вычисляем его позицию и обеспечиваем, чтобы он был виден, вызывая метод QWidget::show(). Устанавливаем атрибут Qt::WA_DeleteOnClose в каждой метке, чтобы обеспечить что любые неиспользуемые метки будут удалены; нам нужно создать новые метки и удалить старые когда они перетащены, и этим гарантируем, что пример не создаст утечки памяти.
Также мы устанавливаем минимальный размер, заголовок окна и палитру виджета FridgeMagnets.
#ifndef Q_WS_S60
QPalette newPalette = palette();
newPalette.setColor(QPalette::Window, Qt::white);
setPalette(newPalette);
#endif
setMinimumSize(400, qMax(200, y));
setWindowTitle(tr("Fridge Magnets"));
В заключение, чтобы дать возможность нашему пользователю перемещать магнитики, мы также должны установить свойство виджета FridgeMagnets, acceptDrops.
setAcceptDrops(true);
}
Установка этого свойства равным true сообщает системе, что этот виджет может принимать события отпускания (события, которые отправляются когда действия перетаскивания завершены). Позже мы реализуем функции, которые гарантируют, что виджет примет интересные ему события отпускания.
Перетаскивание
Давайте взглянем на обработчик события mousePressEvent(), когда начинается операция перетаскивания:
void DragWidget::mousePressEvent(QMouseEvent *event)
{
DragLabel *child = static_cast<DragLabel*>(childAt(event->pos()));
if (!child)
return;
QPoint hotSpot = event->pos() - child->pos();
QByteArray itemData;
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
dataStream << child->labelText() << QPoint(hotSpot);
События мыши происходят, когда нажимается или отпускается кнопка мыши внутри виджета, или когда переместился курсор мыши. Переопределив метод mousePressEvent() мы гарантировали, что мы получим события нажатия кнопок мыши для виджета, содержащего магнитики.
Когда же мы получим такое событие, сначала мы посмотрим не совпадает ли позиция щелчка с одной из меток. Если нет, то мы просто возвратимся.
Если пользователь щелкнул по метке, мы определяем позицию активной точки (позицию щелчка относительно верхнего левого угла метки). Мы создаём байтовый массив для сохранения текста метки и активной точки, а также используем объект QDataStream чтобы направить поток данных в байтовый массив.
Со всей информацией на своём месте мы создаём новый объект QMimeData. Как было сказано выше, объекты QMimeData связывают данные, которые они хранят, с соответствующими MIME-типами, чтобы гарантировать что информация может быть безопасно переслана между приложениями. Метод setData() устанавливает данные, связанные с переданным MIME-типом. В нашем случае мы связываем наш элемент данных с пользовательским типом application/x-fridgemagnet.
QMimeData *mimeData = new QMimeData;
mimeData->setData("application/x-fridgemagnet", itemData);
mimeData->setText(child->labelText());
Обратите внимание на то, что мы связываем текст магнитика с MIME-типом text/plain используя метод QMimeData'а, setText(). Нжиме мы увидим, как наш виджет обнаруживает оба этих MIME-типа с помощью своих обработчиков событий.
В заключение, мы создаём объект QDrag. Класс QDrag, который обрабатывает большинство деталей операции перетаскивания, предоставляя поддержку для передачи данных перетаскивания основанных на MIME. Передаваемые операцией перетаскивания данные содержатся в объекте QMimeData. Когда мы вызываем метод QDrag'а, setMimeData(), право собственности на наш элемент данных передаётся объекту QDrag.
Мы вызываем функцию setPixmap(), чтобы установить растровое изображение, используемое для представления данных во время операции перетаскивания. Обычно это растровое изображение показывает пиктограмму, которая представляет MIME-тип передаваемых данных, но может быть использована любое растровое изображение. В данном примере мы просто используем растровое изображение, используемое самой меткой чтобы сделать её похожей на сам перемещаемый магнитик.
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setPixmap(*child->pixmap());
drag->setHotSpot(hotSpot);
child->hide();
Также мы задаём активную точку, её позицию относительно угла верхнего уровня перетаскиваемого растрового изображения, чтобы она бала вычисленной нами выше точкой. Это делает процесс перетаскивания метки более естественным поскольку курсор всегда указывает на то же место метки во время операции перетаскивания.
Мы начинаем операцию перетаскивания используя функцию QDrag'а, exec(), требуя чтобы магнитик был скопирован когда перетаскивание завершено.
if (drag->exec(Qt::MoveAction | Qt::CopyAction, Qt::CopyAction) == Qt::MoveAction)
child->close();
else
child->show();
}
Функция возвращает действие отпускания выполняемое в настоящее время пользователем (в этом случае это может быть действие копирования или перемещения); если это действие эквивалентно Qt::MoveAction мы закроем активированный виджет магнитика поскольку создаём новый чтобы его заменить (смотрите реализацию dropEvent()). В противном случае, если отпускание происходит за пределами главного виджета, мы просто показываем виджет в его первоначальной позиции.
Отпускание
Когда действие отпускания входит в пределы нашего виджета, мы получаем событие входа перетаскивания. QDragEnterEvent наследует большинство из своей функциональности из класса QDragMoveEvent, который в свою очередь наследует большинство своей функциональности из QDropEvent. Обратите внимание на то, что мы должны принять это событие для того, чтобы получить события перемещения перетаскивания, которые отправляются пока выполняется действие перетаскивания. Событие вхождения перетаскивания всегда непосредственно следует за событием перемещения перетаскивания.
В нашей реализации dragEnterEvent() мы сначала определяем поддерживается ли MIME-тип события или нет:
void DragWidget::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-fridgemagnet")) {
if (children().contains(event->source())) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
Если тип - application/x-fridgemagnet и событие исходит от любых из этих виджетов магнитиков на холодильник в приложении, мы сначала устанавливаем действия отпускания события используя метод QDropEvent::setDropAction(). Действие отпускания события - действие выполняемое с данными цели. Qt::MoveAction показывает, что данные переместились из источник в приёмник.
Затем мы вызываем метод accept() события чтобы показать, что мы обработали событие. Вообще, непринятые события могут передаваться родительскому виджету. Если событие исходит из любого другого виджета, мы просто принимаем предлагаемое действие.
} else if (event->mimeData()->hasText()) {
event->acceptProposedAction();
} else {
event->ignore();
}
}
Мы также принимаем предлагаемое действие если MIME-тип события - text/plain, т.е., если QMimeData::hasText() возвращает true. Если событие имеет любой другой тип, с другой стороны, мы вызываем метод ignore() события позволяя передачу события дальше.
void DragWidget::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-fridgemagnet")) {
if (children().contains(event->source())) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else if (event->mimeData()->hasText()) {
event->acceptProposedAction();
} else {
event->ignore();
}
}
События перемещения перетаскивания происходят, когда курсор входит в пределы виджета, когда он перемещается внутри виджета и когда на клавиатуре нажата клавиша-модификатор в то время, как виджет имеет фокус ввода. Наш виджет будет часто получать события перемещения перетаскивания пока объект перетаскивания находится внутри его границ. Переопределим метод dragMoveEvent(), а также рассмотрим событие точно таким же способом как мы делали с событиями вхождения перетаскивания.
Обратите внимание на то, что обработчик события dropEvent() ведёт себя немного по-другому: Сначала мы получаем MIME-данные события.
void DragWidget::dropEvent(QDropEvent *event)
{
if (event->mimeData()->hasFormat("application/x-fridgemagnet")) {
const QMimeData *mime = event->mimeData();
Класс QMimeData предоставляет контейнер для данных, которые записывать информацию о своём MIME-типе. Объекты QMimeData связывают данные, которые они содержат соответствующие MIME-типы, чтобы гарантировать, что информация может быть безопасно переданы между приложениями, и скопированы внутри одного и того же приложения.
Мы извлекаем данные, связанные с MIME-типом application/x-fridgemagnet используя поток данных для того, чтобы создать новый объект DragLabel.
QByteArray itemData = mime->data("application/x-fridgemagnet");
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
QString text;
QPoint offset;
dataStream >> text >> offset;
Класс QDataStream предоставляет преобразование двоичных данных в последовательную форму для QIODevice (поток данных - двоичный поток закодированной информации, которая полностью независима от основной операционной системы компьютера, ЦПУ или порядка байтов).
В заключение, мы создаём метку и перемещаем её в позицию события:
DragLabel *newLabel = new DragLabel(text, this);
newLabel->move(event->pos() - offset);
newLabel->show();
newLabel->setAttribute(Qt::WA_DeleteOnClose);
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
Если источник события также является виджетом, получающим событие отпускания, мы устанавливаем действие отпускания события равным Qt::MoveAction и вызываем метод accept() события. В противном случае, мы просто принимаем предлагаемое действие. Это означает, что метки перемещаются лучше, чем копируются в том же самом окне. Однако, если мы перетаскиваем метку на второй экземпляр примера "Fridge Magnets", действием по умолчанию является его копирование, оставляя оригинал в первом экземпляре.
Если MIME-тип события - text/plain (т.е., если QMimeData::hasText() возвращает true) мы извлекаем его текст и разделяем его на слова. Для каждого слова создаём новое действие DragLabel и показываем его в позиции события плюс смещение, зависящее от количества слов в тексте. В заключение мы принимаем предлагаемое действие. Это позволяет пользователю отпускать выделенный текст из текстового редактора или веб-браузера в виджет и добавить больше магнитиков.
} else if (event->mimeData()->hasText()) {
QStringList pieces = event->mimeData()->text().split(QRegExp("\\s+"),
QString::SkipEmptyParts);
QPoint position = event->pos();
foreach (QString piece, pieces) {
DragLabel *newLabel = new DragLabel(piece, this);
newLabel->move(position);
newLabel->show();
newLabel->setAttribute(Qt::WA_DeleteOnClose);
position += QPoint(newLabel->width(), 0);
}
event->acceptProposedAction();
} else {
event->ignore();
}
}
Если событие имеет любой другой тип, мы вызываем метод ignore() события, позволяя передачу события далье.
Резюме
Мы установили свойство acceptDrops нашего главного виджета и переопределили обработчики событий QWidget'а - dragEnterEvent(), dragMoveEvent() и dropEvent(), чтобы поддержать содержимое отпускаемое на нашем виджете.
Кроме того, мы переопределили функцию mousePressEvent() чтобы позволить пользователю забрать магнитики в первом месте.
Поскольку данные передаются, используя операции перетаскивания, и кодируются, используя MIME-типы, вы можете запустить более одного экземпляра этого примера и передавать магнитики между ними.
Авторские права © 2010 Nokia Corporation и/или её дочерние компании |
Торговые марки |
Qt 4.6.4 |
|