[SOLVED] making Qpainter graphics clickable



  • Hi,

    I'm new to Qt and have spent a fair amount of time perusing Blanchette/Summerfield as well as this site's instructional materials, but still feel like I'm missing the forest for the trees.

    Currently, I'm trying to figure out how to a associate a Qpainter-drawn shape with the instance of a class, such that if I click the shape, an associated method executes. More concretely, I'm trying to put together a piano-keyboard widget, with a separate graphic/instance associated with each key. What is the "correct" way to think about this?

    If a similar question has already been answered on this board, I apologize in advance.

    Jay



  • I think that the easiest way to do it is to use QGraphicsScene and QGraphicsWidget instead of simple painting.



  • Quessing you've have subclassed QWidget and doing the painting inside paintEvent. To get mouse clicks working you also need to provide an event handler for it. see http://doc.qt.nokia.com/4.7-snapshot/eventsandfilters.html#event-handlers



  • A regular QWidget provides you both with the ability to use QPainter to draw on it directly as well as mouse click event handlers, so you can implement your class by subclassing QWidget, adding the extra functionality and overload the paint and mouse click events and you are good to go. You don't really need the QGraphicsScene/View/Item stack unless you plan view and item transformations such as zoom, rotation and so on...



  • When working with the QWidget approach however, you should be aware that there is no connection whatsoever between what you paint, and how you react to mouse input. Any connection that you want to create, you will have to create yourself. So, you cannot make a QPainter (or what you paint with it) clickable, but you can respond to clicks in your widget in such as way that it seems to the end user that he does click on a shape.

    Using the QGraphics* stack helps you in the sense that it is quite easy to make a mapping between the two, as the framework already handles clicks. Instantiating a QGraphicsItem for each key is a valid approach, IMHO. You don't need to use rotations or other transformations for the framework to be useful.



  • I think GraphicsView is more suitable for what you want to do.

    One thing that might help you, and one that would work in both the QWidget and GraphicsView world:

    If you describe your keys as QPainterPath objects, you can use them both for painting and hit testing.

    See QPainterPath::contains(QPointF)



  • Hello
    Try to look this example "elastic nodes":http://qt-project.org/doc/qt-4.7/graphicsview-elasticnodes.html



  • @Andre - his specific application is a piano simulator - which is pretty much rectangle shape for every key and the click is intercepted by the widget, there is no need to involve the actual shape into event handling, it is purely cosmetic, nor the overhead of the QGraphics* stack.

    All he needs is to implement the pressed/released piano key paint event and repaint the widget upon events. In his scenario I don't think he really needs to have multiple shapes in the widget and be able to distinguish between clicking them, he only needs one QWidged derived key class and to instance it for every key in a layout, which will help him get his keys laid out correctly as the program window size changes - something he won't get with QGrpahicsView. That is why I recommended subclassing QWidget - it suffices, is simpler and he will be able to accomplish his assignment in an easier fashion.

    Edit: Naturally, he will be able to use layout only for the white keys, the black keys he will have to bind to the position of the white keys, since the black keys must be on top of the white and be sort of floating but still positioned by the white keys which are positioned by the layout, it should work well.



  • @ddriver

    a) GraphicsView also has layouts. Laying out the keys would be just as simple in GraphicsView as with widgets
    b) Talking overhead: Creating a QWidget is an expensive operation. By comparison, QGraphicsWidget is cheap as dirt.
    c) GraphicsView is no more complicated than working with widgets. It only depends on what you are used to work with. For someone who has a lot more widget experience than GraphicsView experience, going the QWidget way is probably going to produce results sooner, true.

    Interesting that no one mentioned QML so far.



  • Yes, QML should be the easiest and fastest way, but the OP said he needs QPainter, which could very well mean custom painting in which case he will have to still do his own QML component in C++. But for piano keys, rounded rectangles are good enough IMO, so QML is probably the best solution, plus it supports sound playback too :)



  • Since it is Friday afternoon and I had nothing better to do, here is my pick on a fairly primitive, half-baked QWidget based 1 octave piano keyboard:

    First, the PianoKey class:

    @#ifndef PIANOKEY_H
    #define PIANOKEY_H

    #include <QWidget>

    class PianoKey : public QWidget
    {
    Q_OBJECT
    public:
    explicit PianoKey(bool blackKey, QWidget *parent = 0);

    private:
    bool isPressed, isBlack;

    protected:
    void paintEvent(QPaintEvent *);
    void mousePressEvent(QMouseEvent *);
    void mouseReleaseEvent(QMouseEvent *);
    };

    #endif // PIANOKEY_H@

    @#include "pianokey.h"
    #include <QPainter>

    PianoKey::PianoKey(bool blackKey, QWidget *parent) : QWidget(parent),isPressed(0), isBlack(blackKey) {}

    void PianoKey::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    QColor drawColor;

    if (isBlack)
        if (isPressed)
            drawColor = Qt::gray;
        else
            drawColor = Qt::black;
    else
        if (isPressed)
            drawColor = Qt::lightGray;
        else
            drawColor = Qt::white;
    
    painter.fillRect(rect(), drawColor);
    

    }

    void PianoKey::mousePressEvent(QMouseEvent *) {
    isPressed = 1;
    repaint();
    }

    void PianoKey::mouseReleaseEvent(QMouseEvent *) {
    isPressed = 0;
    repaint();
    }@

    Then, the piano widget:

    @#ifndef WIDGET_H
    #define WIDGET_H

    #include <QWidget>
    #include <QHBoxLayout>
    #include "pianokey.h"

    class Widget : public QWidget
    {
    Q_OBJECT

    public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

    private:
    QHBoxLayout *layout;
    void arrangeBlackKeys();

    QList<PianoKey *> whiteKeys;
    QList<PianoKey *> blackKeys;
    QList<int> blackLocations;
    

    protected:
    void resizeEvent(QResizeEvent *);
    };

    #endif // WIDGET_H@

    @#include "widget.h"

    Widget::Widget(QWidget *parent) : QWidget(parent) {
    layout = new QHBoxLayout(this);

    // create white keys and add to layout
    for (int x = 0; x < 7; ++x) {
        whiteKeys << new PianoKey(0, this);
        layout->addWidget(whiteKeys[x]);
    }
    
    // create black keys
    for (int x = 0; x < 5; ++x)
        blackKeys << new PianoKey(1, this);
    
    // determine which white keys have black keys
    blackLocations << 0 << 1 << 3 << 4 << 5;
    
    resize(800, 600);
    
    //put black keys where they belong
    arrangeBlackKeys();
    

    }

    Widget::~Widget() {}

    void Widget::arrangeBlackKeys() {
    int keyWidth = whiteKeys[0]->width() / 1.6;
    int keyHeight = whiteKeys[0]->height() / 1.6;
    int offset = whiteKeys[0]->width() / 1.5;
    int xLocation, yLocation = whiteKeys[0]->pos().y();

    for (int x = 0; x < blackKeys.length(); ++x) {
        xLocation = blackLocations[x];
        blackKeys[x]->resize(keyWidth, keyHeight);
        blackKeys[x]->move(whiteKeys[xLocation]->pos().x() + offset, yLocation);
    }
    

    }

    void Widget::resizeEvent(QResizeEvent *) {
    arrangeBlackKeys();
    }@

    And finally, the straightforward main.cpp:

    @#include <QtGui/QApplication>
    #include "widget.h"

    int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec&#40;&#41;;
    

    }@



  • I want to thank everyone who responded. Clearly, there are a lot of options for getting this done, which is great if you know your way around, but otherwise rather befuddling.

    @ddriver: Your 130+ lines of code is well beyond any help I could have asked for. I am VERY grateful.

    It's going to take me some time to digest all of this. If necessary I'll follow up with additional questions later.

    Thanks again.



  • That's the "beauty" of programming, often you can do one thing in numerous ways :)

    Be advised, this is a rather" barebone" implementation, it lacks the signals needed to make the keys actually do something besides drawing themselves, also you can't press and slide the mouse accords the keyboard, something you need to sync the keys with their parent widget to accomplish, the keys work like regular push buttons. You will need to add a few signals and slots to communicate between the parent widget and the keys in order to make this a practically useful solution, but it is not that hard. Also you can easily modify it to draw as many keys as you need, I did only one octave just as an example.



  • bq. you can’t press and slide the mouse accords the keyboard, something you need to sync the keys with their parent widget to accomplish[...]You will need to add a few signals and slots to communicate between the parent widget and the keys

    I've been working at this, but haven't yet succeeded. My idea was to move the mouse press/release events up to the parent widget and place a new event handler, mouseMoveEvent, in class PianoKey. As I understand it, any PianoKey instance triggered by mouseMoveEvent needs to signal the remaining PianoKey instances to become inactive, so I need to have PianoKey signal itself. The problem is that when I try to setup a connection in the parent class (Widget), I can't figure out how to direct the signal to every instance of PianoKey, since they're embedded in QLists. Am I on the right track?



  • What if you just handled the mouseMoveEvent in every key, and checked whether the button is currently pressed? In addition, using the leaveEvent, you could release a key when sliding over it.



  • That sounds like a great idea, but I don't understand how I would handle mouseMoveEvent "in every key." Doesn't the event only get triggered in the key that is touched?



  • I think it should work if mouse tracking is enabled (setMouseTracking), but you might have to ungrab the mouse once the cursor leaves the widget - see grabMouse() and releaseMouse()



  • @Asperamanca - handling the press event in the key is like handling a push button's logic inside the push button class. It is simply not the Qt way, the key simply emitting signals that are handled by the parent widget is much more flexible and OOP-friendly.

    planarian - you can just use the leaveEvent and enterEvent. Since with a mouse you have cursor even if you haven't pressed in the key enter and leave event handling you have to "ask" the parent widget if there has been a press event, triggered to true by the first key that gets pressed, and triggered to false by the last key that gets released. If the parent widget returns false enter and leave events do nothing, if the parent widget returns true, then leave event releases a key and the enter event presses a key.

    In the key constructor you can link all keys to a single slot in the parent widget to trigger that bool and another slot which returns that value so that the key event knows how to proceed. You can also have a single parent widget slot to which you connect all keys, with the keys identifying themselves in the signal parameter so that the parent widget slot knows which of them is it, you can just add a private member variable that holds the note of that particular key and sends it to the parent widget slot.



  • Ugh, I can't seem to take more than two steps before bogging down again...currently, I'm struggling to make mousePressEvent work properly with enterEvent/leaveEvent. As the "documentation":http://qt-project.org/doc/qt-4.8/qwidget.html#events indicates, mousePressEvent automatically grabs mouse input, which I assume is why holding the mouse inside the parent prevents the child widgets from issuing enterEvent/leaveEvent as I sweep the pointer across them (Although I would have thought that input grabbed by a parent would still be accessible to its children). Is there a way to reimplement mousePressEvent in a way that keeps it from grabbing input? Is there a better approach?



  • Well, this seems line a nasty limitation... hopefully some of the PRO's can offer a solution

    Meanwhile you could try moving away from using enter and leave events and go back to only the press, release and move event, this time implemented in the parent widget. As you press or move the mouse, you can use the QWidget::childAt ( mouseLocation ) which will return a pointer to the child at that location if any, through which you can change the state of keys and call for their repaint event, check the key note and play the corresponding sound. So the key widget has no even handling at all, you can use qobject_cast to a key to filter out any additional widgets you might have in your piano, keys are only responsible for drawing themselves based on whether they are pressed or not and store their note which is set during the key creation.

    Hope this one works, it is very annoying stumbling upon some of Qt's limitations, which turn out to be surprisingly many considering the size of the framework.



  • I almost forgot, you may want to take a look here: http://vmpk.sourceforge.net/

    It is a piano controller application, the project is quite big and you may have problems finding what you need in there, but it has the type of input you want to achieve. It uses QGraphicsScene thou.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.