2015/05/01

Qt5 Mouse Event 처리


Qt5도 배울 겸 재미삼아 마우스 이벤트 처리 프로그램을 만들어 본다. UI 프로그램을 거의 짜 본적이 없어서 그런지 마우스 이벤트 처리가 생각보다 복잡하다. 소시적부터 관심이 있었으면 Libre Office Impress 같은 것을 만들었을지도... 도형 들을 마우스로 선택하고 옮기고 크기를 조절하고 Widget 창 크기 조절시 창크기에 비례해서 도형 크기도 변해야 한다. 마우스 커서도 선택 모드, 드래그 모드, 리사이즈 모드에 따라 바꿔주고... 우분투의 Screenshot으로 캡춰했더니 마우스 커서는 모양이 안바뀌네...

그런데, Qt에서 사각형 Class인 QRect가 좀 웃긴다. QRect(x, y, width, height)로 생성할 수 있는데 x2 = x+width-1, y2 = y+height-1로 정의하고 있다. 가령, rect.x2()나 rect.y2()를 호출하면 각각 1pixel이 모자라게 된다. 그런데 놀라운 것은 painter.drawRect(rect)로 그리면 1pixel이 모자라지 않게 잘 그려준다. 별 문제가 없어 보이지만, 계산을 할때는 x2 = x+width, y2 = y+height로 계산해야 1pixel에 대해 제대로 고려가 된다. 또, 한가지 이로 인해 파생되는 문제는 rect.normalized()라는 함수가 width나 height가 음수일 때 양수로 사각형 좌표를 바꿔주는데 x와 x2를 swapping 해버림으로써 사각형 크기가 어긋나게 된다.

1pixel 차이 가지고 뭐 그리 쫀쫀하게 구냐고 할지도 모른다. 예전에 MS Powerpoint 발표자료 만들때 그룹 resize 같은 거 하면 pixel이 어긋나는 한 두놈이 꼭 생겨서 그거 맟추느라 고생했던 기억이 남아 있다. 왜냐하면 1pixel이라도 어긋나면 시청자에게 굉장히 허술하게 보이기 때문에... 최근 버전의 Powerpoint에서는 도형을 움직이거나 resize 하면 근처 도형에 정렬할 수 있도록 기능이 강화된 듯하다. 하지만 근본적인 문제가 해결된 것은 아니었다. 이 문제는, 표를 만들 때 무식한 방법으로 사각형 하나를 복제해서 한 줄을 만든 뒤 이 줄을 다시 복제해서 여러 줄을 만들고, 그룹 resize 후에 cell 들을 정렬시키고 나서 Powerpoint Window 화면을 resize해 보면, 금방 확인해 볼 수 있다. 표의 경계선 굵기가 일정하지 않게 되는 문제가 생긴다. Libre Office Impress에서는 resize시에도 정렬이 잘 되는 편이다.

그리고, Qt는 개발자에게 모든 필요한 것을 제공해 주려는 의도가 강해 보인다. 잡다하게 많은 Class 들이 뭐 썩먹을 수야 있지만 좀 난잡해 보인달까... 가령, int 버전의 QRect와 qreal(float) 버전의 QRectF와 같이 종류별로 int 버전과 qreal 버전을 구비하고 있다. 화면에서의 pixel들은 궁극적으로 int형이긴 하지만 resize나 reshaping 같은 것을 하기 위해서는 qreal 버전이 필요하기는 하다. 위의 pixel이 어긋나는 문제가 생기는 원인도 실수를 정수로 변환하면서 발생하는 rounding 오류 때문에 생긴다. 해결 방법은 가능한한 rounding 오류가 발생하지 않도록 해주면 된다. 가령, 특정 정수의 배수를 사용하여 나머지가 항상 0이 되도록 만들어 준다든지...


Mouse Press Event
void ChartWidget::mousePressEvent(QMouseEvent* event)
{
    m_firstPos = boundPos(event->pos());
    m_lastPos = m_firstPos;

    bool newPos = true;
    bool selectStat = false;
    bool modKey = event->modifiers().testFlag(Qt::ShiftModifier);
    bool itemSelected = m_selectedItems.size() ? true : false;
    QVariant info;

    if(itemSelected) {
        // if mod key pressed, deselect the selected item.
        if(modKey) {
            foreach(Drawable* drawable, m_selectedItems) {
                if(drawable->contains(m_firstPos, &info)) {
                    removeSelectedItem(drawable);
                    selectStat |= drawable->onDeselect();
                    newPos = false;
                    break;
                }
            }
        }
        else {
            // if reclick one of selected items, check new mouse event.
            foreach(Drawable* drawable, m_selectedItems) {
                if((m_resizeIndex = drawable->selectIndex(m_firstPos))) {
                    m_resizeMode = true;
                    m_mouseItem = drawable;
                    return;
                }
                if(drawable->contains(m_firstPos, &info)) {
                    m_dragMode = true;
                    return;
                }
            }
            // if new mouse position, deselect all the selected items.
            foreach(Drawable* drawable, m_selectedItems) {
                removeSelectedItem(drawable);
                selectStat |= drawable->onDeselect();
            }
        }
    }

    if(!itemSelected || newPos) {
        Drawable* clicked = drawableAt(m_firstPos, &info);
        // if new item is clicked select the item.
        if(clicked) {
            m_selectMode = false;
            m_dragMode = true;
            addSelectedItem(clicked);
            selectStat |= clicked->onSelect(event, modKey, info);
        }
        // if no item is selected check new select event.
        else m_selectMode = true;
    }

    if(selectStat) emit selectionChanged();
    if(!m_selectMode) reDraw();
    QWidget::mousePressEvent(event);
}
Mouse Move Event
void ChartWidget::mouseMoveEvent(QMouseEvent* event)
{
    m_lastPos = boundPos(event->pos());

    m_cursor = mcPointer;
    if(m_selectedItems.size()) {
        if(m_resizeMode) {
            m_cursor = (m_resizeIndex < 3) ? mcSizeLCross :
                       (m_resizeIndex < 5) ? mcSizeRCross :
                       (m_resizeIndex < 7) ? mcSizeVert :
                       mcSizeHoriz;
            m_selectRect = m_mouseItem->region();
            foreach(Drawable* drawable, m_selectedItems) drawable->onResize(m_resizeIndex);
            m_selectRect = m_mouseItem->region();
        }
        else if(m_dragMode) {
            m_cursor = mcDrag;
            foreach(Drawable* drawable, m_selectedItems) drawable->onMouseMove(event);
            m_firstPos = m_lastPos;
        }
        else {
            foreach(Drawable* drawable, m_selectedItems) {
                if((m_resizeIndex = drawable->selectIndex(m_lastPos))) {
                    m_cursor = (m_resizeIndex < 3) ? mcSizeLCross :
                               (m_resizeIndex < 5) ? mcSizeRCross :
                               (m_resizeIndex < 7) ? mcSizeVert :
                               mcSizeHoriz;
                    break;
                }
                if(drawable->contains(m_lastPos)) {
                    m_cursor = mcDrag;
                    break;
                }
            }
        }

    }

    reDraw();
    QWidget::mouseMoveEvent(event);
}
void Drawable::onMouseMove(QMouseEvent* event)
{
    Q_UNUSED(event)
    
    QRect rect = region();
    QPoint pos = rect.topLeft() + owner()->lastPos() - owner()->firstPos();
    setRegion(QRect(pos, rect.size()));
}
Mouse Release Event
void ChartWidget::mouseReleaseEvent(QMouseEvent* event)
{
    m_lastPos = boundPos(event->pos());
    //m_lastPos = adjustPosition(m_lastPos);

    bool modKey = event->modifiers().testFlag(Qt::ControlModifier);

    // if mouse left-click position changed.
    if(event->button()==Qt::LeftButton && (m_firstPos-m_lastPos).manhattanLength()>TOL_SELECT) {
        if(!modKey && m_selectedItems.size()) {
            foreach(Drawable* drawable, m_selectedItems) drawable->onMouseRelease(event);
        }
        else {
            bool selectStat = false;
            QVariant info = 0;
            QRect rect = QRect(m_firstPos, m_lastPos);
            foreach(Layer* layer, m_layers) {
                foreach(Drawable* drawable, layer->children()) {
                    // if non-selected items are in mouse dragged region, select new items.
                    if(rect.contains(drawable->region())) {
                        addSelectedItem(drawable);
                        selectStat |= drawable->onSelect(event, false, info);
                    }
                }
            }
            if(selectStat) emit selectionChanged();
        }
    }

    m_selectMode = false;
    m_dragMode = false;
    m_resizeMode = false;
    m_resizeIndex = 0;
    if(m_mouseItem) m_mouseItem = 0;
    reDraw();
    QWidget::mouseReleaseEvent(event);
}
void Drawable::onMouseRelease(QMouseEvent* event)
{
    Q_UNUSED(event)
    
    setRegion(positiveRect(m_rect));
    QVector2D ar = owner()->aspectRatio();
    m_rectOrigin.setWidth(m_rect.width()/ar.x());
    m_rectOrigin.setHeight(m_rect.height()/ar.y());
}
Widget Resize Event
void ChartWidget::resizeEvent(QResizeEvent* event)
{
    if(m_pixmap) delete m_pixmap;
    m_pixmap = new QPixmap(event->size());

    setViewportChanged();
    setViewport(rect());
    qreal arX = m_viewport.width() / m_viewportOrigin.width();
    qreal arY = m_viewport.height() / m_viewportOrigin.height();
    setAspectRatio(arX, arY);

    reDraw();
    setViewportChanged(false);
}
void Drawable::update()
{
    QVector2D ar = owner()->aspectRatio();
    qreal arX = ar.x();
    qreal arY = ar.y();
    if(owner()->viewportChanged()) {
        qreal x1 = arX * m_posOrigin.x();
        qreal y1 = arY * m_posOrigin.y();
        qreal x2 = x1 + arX * m_rectOrigin.width();
        qreal y2 = y1 + arY * m_rectOrigin.height();
        m_posCurrent = QPointF(x1, y1);
        // rounding errors are increased if you rsize chartwidget frequently.
        m_rect = QRect(RoundToMultiple(x1), RoundToMultiple(y1), 
                       RoundToMultiple(x2-x1), RoundToMultiple(y2-y1));
    }
    else {
        m_posCurrent = QPointF(m_rect.x(), m_rect.y());
        m_posOrigin = QPointF(m_posCurrent.x()/arX, m_posCurrent.y()/arY);
    }
}

댓글 없음:

댓글 쓰기