Wiki

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

Автор: Andi Peredri

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

Scale


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

Scale Screenshot

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


  • класс ScaleItem является потомком QCanvasRectangle и обеспечивает отрисовку и масштабирование планет;
  • класс ScaleView является потомком QCanvasView и содержит функции, обрабатывающие события мыши, выбор планет и отображение их координат;
  • класс Scale (QMainWindow ) является окном приложения, он содержит Canvas-вид и меню.

Конструктор Scale создает объект QCanvas и настраивает ScaleView для его просмотра. Также мы создаем и инициализируем планеты, и устанавливаем их начальное положение и размеры.
Scale::Scale() : QMainWindow()
{
    <a href="http://doc.trolltech.com/qwidget.html#setCaption">setCaption</a>("Scale Example");
 
    // Создаем и настраиваем QCanvas и ScaleView
    QCanvas* canvas = new <a href="http://doc.trolltech.com/qcanvas.html#QCanvas">QCanvas</a>(this);
    canvas-><a href="http://doc.trolltech.com/qcanvas.html#setBackgroundColor">setBackgroundColor</a>(black);
    canvas-><a href="http://doc.trolltech.com/qcanvas.html#resize">resize</a>(500, 370);
    ScaleView* view = new ScaleView(canvas, this);
    <a href="http://doc.trolltech.com/qmainwindow.html#setCentralWidget">setCentralWidget</a>(view);
 
    // Добавляем планеты
    new ScaleItem("Sun",     <a href="http://doc.trolltech.com/qimage.html#QImage">QImage</a>("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::<a href="http://doc.trolltech.com/qscrollview.html#contentsMousePressEvent">contentsMousePressEvent</a>(<a href="http://doc.trolltech.com/qmouseevent.html">QMouseEvent</a>* e)
{
    pressed = true;
    int cursor = <a href="http://doc.trolltech.com/qscrollview.html#viewport">viewport</a>()-><a href="http://doc.trolltech.com/qwidget.html#cursor">cursor</a>().<a href="http://doc.trolltech.com/qcursor.html#shape">shape</a>(); // Определяем форму курсора
 
    if( cursor == <a href="http://doc.trolltech.com/qt.html#CursorShape-enum">SizeVerCursor</a> || cursor == <a href="http://doc.trolltech.com/qt.html#CursorShape-enum">SizeFDiagCursor</a> ||
        cursor == <a href="http://doc.trolltech.com/qt.html#CursorShape-enum">SizeHorCursor</a> || cursor == <a href="http://doc.trolltech.com/qt.html#CursorShape-enum">SizeBDiagCursor</a>)
    {
        // Осуществляем захват границы планеты, сохраняем положение курсора.
        selectedPos = e-><a href="http://doc.trolltech.com/qmouseevent.html#pos">pos</a>();
        return;
    }
 
    // Осуществляем выбор планеты
    // Получаем список всех объектов QCanvasItem, находящихся под курсором
    <a href="http://doc.trolltech.com/qcanvasitemlist.html">QCanvasItemList</a> l = <a href="http://doc.trolltech.com/qcanvasview.html#canvas">canvas</a>()-><a href="http://doc.trolltech.com/qcanvas.html#collisions">collisions</a>(e->pos());
    for(QCanvasItemList::Iterator it = l.<a href="http://doc.trolltech.com/qvaluelist.html#begin">begin</a>(); it != l.<a href="http://doc.trolltech.com/qvaluelist.html#end">end</a>(); ++it)
    {
        // Выбираем только планеты
        if((*it)-><a href="http://doc.trolltech.com/qcanvasitem.html#rtti">rtti</a>() != ItemRtti) continue;
        ScaleItem* si = (ScaleItem*)(*it);
 
        // Выбранная точка не должна быть прозрачной
        if(!si->hit(e->pos())) continue;
        selectedPos = e->pos();
        selectItem(si);        // делаем выбор
        return;
    }
    // Под курсором не обнаружено ни одной планеты
    selectItem(0);
}

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

  • ArrowCursor - стандартный курсор-стрелка. Служит для перемещения объектов.
  • SizeHorCursor - горизонтальный курсор. Служит для изменения ширины объекта.
  • SizeVerCursor - вертикальный курсор. Служит для изменения высоты объекта.
  • SizeBDiagCursor - диагональный курсор (/). Используется для одновременного изменения ширины и высоты объекта.
  • SizeFDiagCursor - диагональный курсор (\). Используется аналогично предыдущему.

Также он обеспечивает перемещение и масштабирование выбранной планеты. Здесь же задается порог чувствительности мыши и минимальный размер планеты. Обратите внимание, что при перемещении левого или верхнего края планеты осуществляется одновременное масштабирование и перемещение планеты в новое положение.
void ScaleView::<a href="http://doc.trolltech.com/qscrollview.html#contentsMouseMoveEvent">contentsMouseMoveEvent</a>(QMouseEvent* e)
{
    if(!selected) return; // Ни одна из планет не выбрана
    if(!pressed)          // Если мы не удерживаем планету...
    {
        // ...то определяем положение указателя мыши относительно нее
        static const int sens = 3;         // Чувствительность мыши
        int x = e-><a href="http://doc.trolltech.com/qmouseevent.html#x">x</a>();
        int y = e-><a href="http://doc.trolltech.com/qmouseevent.html#y">y</a>();
        int x1 = (int) selected-><a href="http://doc.trolltech.com/qcanvasitem.html#x">x</a>();      // Наружный левый край
        int x2 = x1 + sens;                // Внутренний левый край
        int x4 = x1 + selected-><a href="http://doc.trolltech.com/qcanvasrectangle.html#width">width</a>();   // Наружный правый край
        int x3 = x4 - sens;                // Внутренний правый край
        int y1 = (int) selected-><a href="http://doc.trolltech.com/qcanvasitem.html#y">y</a>();      // Наружный верхний край
        int y2 = y1 + sens;                // Внутренний верхний край
        int y4 = y1 + selected-><a href="http://doc.trolltech.com/qcanvasrectangle.html#height">height</a>();  // Наружный нижний край
        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()-><a href="http://doc.trolltech.com/qwidget.html#setCursor">setCursor</a>(cursor); // Устанавливаем форму курсора мыши
    }
    else  // Мы удерживаем планету
    {
        static const int minimum = 8;   // Минимальный размер планеты
        <a href="http://doc.trolltech.com/qpoint.html">QPoint</a> 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.<a href="http://doc.trolltech.com/qpoint.html#x">x</a>() > selected-><a href="http://doc.trolltech.com/qcanvasrectangle.html#rect">rect</a>().<a href="http://doc.trolltech.com/qrect.html#center">center</a>().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.<a href="http://doc.trolltech.com/qpoint.html#setX">setX</a>(QMIN(e->x(), selected->x()));
            }
 
        // Требуется изменить высоту планеты
        if(cursor == SizeVerCursor || cursor == SizeFDiagCursor || cursor == SizeBDiagCursor)
 
            // Курсор находится ниже центра планеты
            if(selectedPos.<a href="http://doc.trolltech.com/qpoint.html#y">y</a>() > 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.<a href="http://doc.trolltech.com/qpoint.html#setY">setY</a>(QMIN(e->y(), selected->y()));
            }
 
        // Необходимо переместить планету
        if(cursor == ArrowCursor)
        {
            selected-><a href="http://doc.trolltech.com/qcanvasitem.html#moveBy">moveBy</a>(offset.x(), offset.y());
            selectedPos = e->pos();
        }
        else
            selected->scale(w, h); // Осуществляем масштабирование планеты
        updateInformation();       // Обновляем информацию о планете
        canvas()-><a href="http://doc.trolltech.com/qcanvas.html#update">update</a>();        // Обновляем содержимое QCanvas
    }
}

ScaleItem


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

  • QImage::scale() использует более простой и быстрый алгоритм. Однако она дает плохие результаты при размерах изображения меньше начальных.
  • QImage::smoothScale() обеспечивает сглаживание при масштабировании. Однако при размерах изображения больше начальных предоставляемое ею качество сопоставимо с QImage::scale(), в то время, как производительность значительно ниже.

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

  • QImage::ScaleFree - режим масштабирования изображений без сохранения пропорций.
  • QImage::ScaleMin - режим масштабирования изображений с сохранением пропорций. Один из размеров полученного изображения может оказаться меньше требуемого.
  • QImage::ScaleMax - в этом режиме также обеспечивается сохранение пропорций изображения. Однако один из размеров полученного изображения может оказаться больше требуемого.

Класс ScaleItem обеспечивает отрисовку планет и масштабирование изображений (функции drawShape() и scale()). Функция hit() проверяет, попали ли мы в прозрачную область планеты. Функция adjust() производит выравнивание планеты по границам видимой области QCanvas. Для повышения производительности класс ScaleItem содержит два изображения: оригинальное и масштабированное.
void ScaleItem::<a href="http://doc.trolltech.com/qcanvasrectangle.html#drawShape">drawShape</a>(<a href="http://doc.trolltech.com/qpainter.html">QPainter</a>& p)
{
    // Прорисовка масштабированного изображения
    p.<a href="http://doc.trolltech.com/qpainter.html#drawImage">drawImage</a>(x(), y(), scaled, 0, 0, width(), height());
 
    if(isSelected()) p.<a href="http://doc.trolltech.com/qpainter.html#drawRect">drawRect</a>(rect()); // Прорисовка размерной рамки
}
 
 
void ScaleItem::scale(int w, int h)
{
    // Создание масштабированного изображения
    if(scaling == Simple) // Используется быстрое масштабирование
        scaled = image.scale(w, h, mode);
    else                  // Используется масштабирование со сглаживанием
        scaled = image.smoothScale(w, h, mode);
    <a href="http://doc.trolltech.com/qcanvasrectangle.html#setSize">setSize</a>(w, h);
    <a href="http://doc.trolltech.com/qcanvasitem.html#update">update</a>();
}
 
 
bool ScaleItem::hit(const QPoint& p) const
{
    // Проверка прозрачности в данной точке изображения
    int dx = p.x() - (int)x();
    int dy = p.y() - (int)y();
    if(!scaled.<a href="http://doc.trolltech.com/qimage.html#valid">valid</a>(dx, dy)) return false;
    return <a href="http://doc.trolltech.com/qcolor.html#qAlpha">qAlpha</a>(scaled.<a href="http://doc.trolltech.com/qimage.html#pixel">pixel</a>(dx, dy));
}
 
 
void ScaleItem::adjust()
{
    // Сокращение размеров объекта до размеров изображения планеты
    setSize(QMIN(width(), scaled.<a href="http://doc.trolltech.com/qimage.html#width">width</a>()), QMIN(height(), scaled.<a href="http://doc.trolltech.com/qimage.html#height">height</a>()));
 
    // Выравнивание планеты по границам области QCanvas
    int xmax = canvas()-><a href="http://doc.trolltech.com/qcanvas.html#width">width</a>() - width();
    int ymax = canvas()-><a href="http://doc.trolltech.com/qcanvas.html#height">height</a>() - height();
    if(x() > xmax) setX(xmax);
    if(y() > ymax) setY(ymax);
    if(x() < 0)    setX(0);
    if(y() < 0)    setY(0);
}