Масштабирование QCanvasItem-объектов

Andi Peredri

Довольно часто при работе с классом QCanvas требуется обеспечить индивидуальное ручное масштабирование элементов QCanvasItem. В этой статье показано, как реализуется выбор, перемещение и масштабирование элементов QCanvasItem на примере небольшой демонстрационной программы.

Scale

В программе Scale представлено двухмерное пространство и некоторое число объектов (планет). Объекты можно свободно выбирать, перемещать и масштабировать с помощью мыши. Положение, название и текущий размер объекта отображаются в верхнем левом углу главного окна программы. С помощью меню может быть выбрана одна из двух функций и один из трех режимов масштабирования.

Scale Screenshot

Мы используем 3 класса:

Конструктор Scale создает объект QCanvas и настраивает ScaleView для его просмотра. Также мы создаем и инициализируем планеты, и устанавливаем их начальное положение и размеры.

Scale::Scale() : QMainWindow()
{
    setCaption("Scale Example");

    // Создаем и настраиваем QCanvas и ScaleView
    QCanvas* canvas = new QCanvas(this);
    canvas->setBackgroundColor(black);
    canvas->resize(500, 370);
    ScaleView* view = new ScaleView(canvas, this);
    setCentralWidget(view);

    // Добавляем планеты
    new ScaleItem("Sun",     QImage("pics/sun.png"), 300, 150, 50, 50, canvas);
    new ScaleItem("Mars",    QImage("pics/mars.png"), 100, 100, 30, 30, canvas);
    new ScaleItem("Saturn",  QImage("pics/saturn.png"), 400, 50, 50, 50, canvas);
    new ScaleItem("Jupiter", QImage("pics/jupiter.png"), 50, 200, 150, 150, canvas);
    new ScaleItem("Mercury", QImage("pics/mercury.png"), 300, 200, 10, 10, canvas);
    new ScaleItem("Neptune", QImage("pics/neptune.png"), 350, 250, 30, 30, canvas);

...

}

ScaleView

Конструктор ScaleView создает три текстовых объекта QCanvasText в верхнем левом углу QCanvas, предназначенные для вывода информации о выбранной планете, и включает режим отслеживания перемещений мыши. Выбор планеты и обеспечение захвата ее границ осуществляется обработчиком событий contentsMousePressEvent().

void ScaleView::contentsMousePressEvent(QMouseEvent* e)
{
    pressed = true;
    int cursor = viewport()->cursor().shape(); // Определяем форму курсора

    if( cursor == SizeVerCursor || cursor == SizeFDiagCursor ||
        cursor == SizeHorCursor || cursor == SizeBDiagCursor)
    {
        // Осуществляем захват границы планеты, сохраняем положение курсора.
        selectedPos = e->pos();
        return;
    }

    // Осуществляем выбор планеты
    // Получаем список всех объектов QCanvasItem, находящихся под курсором
    QCanvasItemList l = canvas()->collisions(e->pos());
    for(QCanvasItemList::Iterator it = l.begin(); it != l.end(); ++it)
    {
        // Выбираем только планеты
        if((*it)->rtti() != ItemRtti) continue;
        ScaleItem* si = (ScaleItem*)(*it);

        // Выбранная точка не должна быть прозрачной
        if(!si->hit(e->pos())) continue;
        selectedPos = e->pos();
        selectItem(si);        // делаем выбор
        return;
    }
    // Под курсором не обнаружено ни одной планеты
    selectItem(0);
}

Обработчик событий contentsMouseMoveEvent() определяет положение указателя мыши относительно границ планеты и устанавливает соответствующую форму курсора:

Также он обеспечивает перемещение и масштабирование выбранной планеты. Здесь же задается порог чувствительности мыши и минимальный размер планеты. Обратите внимание, что при перемещении левого или верхнего края планеты осуществляется одновременное масштабирование и перемещение планеты в новое положение.

void ScaleView::contentsMouseMoveEvent(QMouseEvent* e)
{
    if(!selected) return; // Ни одна из планет не выбрана
    if(!pressed)          // Если мы не удерживаем планету...
    {
        // ...то определяем положение указателя мыши относительно нее
        static const int sens = 3;         // Чувствительность мыши
        int x = e->x();
        int y = e->y();
        int x1 = (int) selected->x();      // Наружный левый край
        int x2 = x1 + sens;                // Внутренний левый край
        int x4 = x1 + selected->width();   // Наружный правый край
        int x3 = x4 - sens;                // Внутренний правый край
        int y1 = (int) selected->y();      // Наружный верхний край
        int y2 = y1 + sens;                // Внутренний верхний край
        int y4 = y1 + selected->height();  // Наружный нижний край
        int y3 = y4 - sens;                // Внутренний нижний край

        int cursor = ArrowCursor; // Форма курсора мыши по-умолчанию
        if(x > x4 || y > y4);     // Ничего не делаем, если курсор за пределами планеты
        else if(x > x3)
            if(y > y3)      cursor = SizeFDiagCursor;  // Курсор над нижним правым углом
            else if(y > y2) cursor = SizeHorCursor;    // Курсор над правым краем
            else if(y > y1) cursor = SizeBDiagCursor;  // Курсор над верхним правым углом
            else ;
        else if(x > x2)
            if(y > y3)      cursor = SizeVerCursor;    // Курсор над нижним краем
            else if(y > y2) ;                          // Курсор над внутренней частью планеты
            else if(y > y1) cursor = SizeVerCursor;    // Курсор над верхним краем
            else ;
        else if(x > x1)
            if(y > y3)      cursor = SizeBDiagCursor;  // Курсор над нижним левым углом
            else if(y > y2) cursor = SizeHorCursor;    // Курсор над левым краем
            else if(y > y1) cursor = SizeFDiagCursor;  // Курсор над верхним левым углом
            else ;
        else ;
        viewport()->setCursor(cursor); // Устанавливаем форму курсора мыши
    }
    else  // Мы удерживаем планету
    {
        static const int minimum = 8;   // Минимальный размер планеты
        QPoint offset = e->pos() - selectedPos;    // Смещение мыши
        int w = selected->width();
        int h = selected->height();
        int cursor = viewport()->cursor().shape(); // Определяем форму курсора

        // Требуется изменить ширину планеты
        if(cursor == SizeHorCursor || cursor == SizeFDiagCursor || cursor == SizeBDiagCursor)

            // Курсор находится справа от центра планеты
            if(selectedPos.x() > selected->rect().center().x())
            {
                w = QMAX(w + offset.x(), minimum);
                selectedPos.setX(QMAX(selected->x() + minimum, e->x()));
            }
            else // Курсор находится слева от центра планеты
            {
                selected->setX(QMIN(selected->x() + offset.x(),
                                    selected->rect().right() - minimum + 1));
                w = QMAX(w - offset.x(), minimum);
                selectedPos.setX(QMIN(e->x(), selected->x()));
            }

        // Требуется изменить высоту планеты
        if(cursor == SizeVerCursor || cursor == SizeFDiagCursor || cursor == SizeBDiagCursor)

            // Курсор находится ниже центра планеты
            if(selectedPos.y() > selected->rect().center().y())
            {
                h = QMAX(h + offset.y(), minimum);
                selectedPos.setY(QMAX(selected->y() + minimum, e->y()));
            }
            else // Курсор находится выше центра планеты
            {
                selected->setY(QMIN(selected->y() + offset.y(),
                                    selected->rect().bottom() - minimum + 1));
                h = QMAX(h - offset.y(), minimum);
                selectedPos.setY(QMIN(e->y(), selected->y()));
            }

        // Необходимо переместить планету
        if(cursor == ArrowCursor)
        {
            selected->moveBy(offset.x(), offset.y());
            selectedPos = e->pos();
        }
        else
            selected->scale(w, h); // Осуществляем масштабирование планеты
        updateInformation();       // Обновляем информацию о планете
        canvas()->update();        // Обновляем содержимое QCanvas
    }
}

ScaleItem

Для осуществления масштабирования изображений Qt предлагает две функции:

Различие в работе функций scale и smoothScale наиболее заметно в получаемом изображении "колец Сатурна". Функции масштабирования могут работать в одном из трех режимов:

Класс ScaleItem обеспечивает отрисовку планет и масштабирование изображений (функции drawShape() и scale()). Функция hit() проверяет, попали ли мы в прозрачную область планеты. Функция adjust() производит выравнивание планеты по границам видимой области QCanvas. Для повышения производительности класс ScaleItem содержит два изображения: оригинальное и масштабированное.

void ScaleItem::drawShape(QPainter& p)
{
    // Прорисовка масштабированного изображения
    p.drawImage(x(), y(), scaled, 0, 0, width(), height());

    if(isSelected()) p.drawRect(rect()); // Прорисовка размерной рамки
}


void ScaleItem::scale(int w, int h)
{
    // Создание масштабированного изображения
    if(scaling == Simple) // Используется быстрое масштабирование
        scaled = image.scale(w, h, mode);
    else                  // Используется масштабирование со сглаживанием
        scaled = image.smoothScale(w, h, mode);
    setSize(w, h);
    update();
}


bool ScaleItem::hit(const QPoint& p) const
{
    // Проверка прозрачности в данной точке изображения
    int dx = p.x() - (int)x();
    int dy = p.y() - (int)y();
    if(!scaled.valid(dx, dy)) return false;
    return qAlpha(scaled.pixel(dx, dy));
}


void ScaleItem::adjust()
{
    // Сокращение размеров объекта до размеров изображения планеты
    setSize(QMIN(width(), scaled.width()), QMIN(height(), scaled.height()));

    // Выравнивание планеты по границам области QCanvas
    int xmax = canvas()->width() - width();
    int ymax = canvas()->height() - height();
    if(x() > xmax) setX(xmax);
    if(y() > ymax) setY(ymax);
    if(x() < 0)    setX(0);
    if(y() < 0)    setY(0);
}

Исходный код программы Scale: scale.tar.gz