3

I would like to know the "best practice" to change the behavior of some buttons to do the following:

I want with a click to appear a menu. Or when you drag this same button you could drop it in another and this will "draw" a line connecting them.

Here is an Example: enter image description here The idea is to connect those "jack" buttons to any other "input" buttons.

I was using the Qt designer and I realize that the buttons properties only the "acceptDrops" property is listed, but I can't make it work. Signals/Slots doesn't list something about dragging or dropping.

So I think that the only way to do it is creating a "Custom Widget" or "reimplementing" a button by code. Maybe the same thing with Signals/Slots

What is the best approach to do so if I don't want to modify the pyuic generated file?

UPDATE: The approach that I tried is by using Qt designer and the "Promoted widgets" option. That allows me to create separate class files and reimplement some elements. I already tested by promoting a PushButton to a "DragButton" and created a class for it:

from PyQt4 import QtGui, QtCore

class DragButton(QtGui.QPushButton):

def __init__(self, parent):
     super(DragButton,  self).__init__(parent)
     self.allowDrag = True

def setAllowDrag(self, allowDrag):
    if type(allowDrag) == bool:
       self.allowDrag = allowDrag
    else:
        raise TypeError("You have to set a boolean type")

def mouseMoveEvent(self, e):
    if e.buttons() != QtCore.Qt.RightButton:
        return

    if self.allowDrag == True:
        # write the relative cursor position to mime data
        mimeData = QtCore.QMimeData()
        # simple string with 'x,y'
        mimeData.setText('%d,%d' % (e.x(), e.y()))
        print mimeData.text()

        # let's make it fancy. we'll show a "ghost" of the button as we drag
        # grab the button to a pixmap
        pixmap = QtGui.QPixmap.grabWidget(self)

        # below makes the pixmap half transparent
        painter = QtGui.QPainter(pixmap)
        painter.setCompositionMode(painter.CompositionMode_DestinationIn)
        painter.fillRect(pixmap.rect(), QtGui.QColor(0, 0, 0, 127))
        painter.end()

        # make a QDrag
        drag = QtGui.QDrag(self)
        # put our MimeData
        drag.setMimeData(mimeData)
        # set its Pixmap
        drag.setPixmap(pixmap)
        # shift the Pixmap so that it coincides with the cursor position
        drag.setHotSpot(e.pos())

        # start the drag operation
        # exec_ will return the accepted action from dropEvent
        if drag.exec_(QtCore.Qt.LinkAction | QtCore.Qt.MoveAction) == QtCore.Qt.LinkAction:
            print 'linked'
        else:
            print 'moved'

def mousePressEvent(self, e):
    QtGui.QPushButton.mousePressEvent(self, e)
    if e.button() == QtCore.Qt.LeftButton:
        print 'press'
        #AQUI DEBO IMPLEMENTAR EL MENU CONTEXTUAL

def dragEnterEvent(self, e):
    e.accept()

def dropEvent(self, e):
    # get the relative position from the mime data
    mime = e.mimeData().text()
    x, y = map(int, mime.split(','))

        # move
        # so move the dragged button (i.e. event.source())
    print e.pos()
        #e.source().move(e.pos()-QtCore.QPoint(x, y))
        # set the drop action as LinkAction
    e.setDropAction(QtCore.Qt.LinkAction)
    # tell the QDrag we accepted it
    e.accept()

I got some hints and take snippets from this post: PyQt4 - Drag and Drop

At this point I'm able to drag this button, and drop it into another of the same type that have the "acceptDrops" property set true in Qt designer. However, I still want to restrict dragging of some buttons (perhaps by setting at the main file with UpdateUi method) because some will be just for accepting drops

UPDATE 2: Now I'm trying to write a class which paint lines or "wires" connecting those buttons.

I'm trying to draw a line between two widgets (two pushbuttons) into a graphicsView with their positions as reference. But when I try, the line is drawn in a wrong place. I also tried using functions like mapToGlobal or mapToParent with different results, but still wrong. In the same class I have another method that draws lines with the mouse, and works ok. I was taking it like a reference or example, but it seems that the events position has a different coordinate system. Well, i don't know why is happening this.

The buttons and the graphicsview are inside a Widget, which is also inside a Window.

Here it is the class, the method that we are talking about is from PyQt4 import QtGui, QtCore

class WiringGraphicsView(QtGui.QGraphicsView):

    def __init__(self, parent):
        QtGui.QGraphicsView.__init__(self, parent)
        self.setScene(QtGui.QGraphicsScene(self))
        self.setSceneRect(QtCore.QRectF(self.viewport().rect()))

    def mousePressEvent(self, event):
        self._start = event.pos()

    def mouseReleaseEvent(self, event):
        start = QtCore.QPointF(self.mapToScene(self._start))
        end = QtCore.QPointF(self.mapToScene(event.pos()))
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
        pen = QtGui.QPen(brush, 2)
        line = QtGui.QGraphicsLineItem(QtCore.QLineF(start, end))
        line.setPen(pen)
        self.scene().addItem( line )

    def paintWire(self, start_widget,  end_widget):
        start_position = QtCore.QPointF(self.mapToScene(start_widget.pos()))
        end_position = QtCore.QPointF(self.mapToScene(end_widget.pos()))
        brush = QtGui.QBrush(QtGui.QColor(255, 0, 0) )
        pen = QtGui.QPen(brush, 2)
        line = QtGui.QGraphicsLineItem(QtCore.QLineF(start_position, end_position))
        line.setPen(pen)
        self.scene().addItem( line )

If is there any better way to implement this, please tell me.

Community
  • 1
  • 1
Mr_LinDowsMac
  • 2,644
  • 9
  • 56
  • 75
  • Could you elaborate on: "I want with a simple click to appear a menu." and "when you drag the same button you could drop it in another and this will "draw" a line connecting them. " Where do you want to click? On a Button? Why don´t you use a dropdown menu? What do you mean by "drop button on another?" where shoulde the button be positioned? Maybe you could draw a wireframe? – Mailerdaimon Feb 03 '15 at 07:28
  • I think you're right, maybe what I need is a dropdown menu, but as an additional feature, I need to drag this button and then drop it into any other similar buttons positioned in other places on the form. Any drag n drop of those buttons will draw a line connecting them... or any selection in the dropdown menu will do the same thing. So, after any "line connection" (or disconnection) of those widgets, I also need a way to catch that event to code a proper action for it. – Mr_LinDowsMac Feb 03 '15 at 08:06
  • @Mailerdaimon Something like this:[link]https://www.dropbox.com/s/li5xuz574r91cgz/mixerwindow.png?dl=0 That is just an example, the GUI will be have a lot more items than that. – Mr_LinDowsMac Feb 03 '15 at 08:39
  • If you upload the image to a public service i could add it to your post. – Mailerdaimon Feb 03 '15 at 08:42
  • @Mailerdaimon I uploaded it here: http://i.minus.com/iwUc5wK0PNXlu.png – Mr_LinDowsMac Feb 03 '15 at 08:58

4 Answers4

2

In order to add code to a UI generated with QtDesigner, you must generate a .py file using pyuic:

pyuic myform.ui -o ui_myform.py

This ui_myform.py file, contains generated code that you should not edit, so later you can change your .ui file with QtDesigner, re-run pyuic, and get ui_myform.py updated without loosing any work.

The generated file will have a class Ui_myForm(object) (named after on your main widget's name), with a def setupUi(self, myForm) method inside. One way this can be used, is by creating your own class MyForm (on a separate file) which will inherit Ui_myForm, and some other Qt Class, like QWidget or QDialog:

myform.py:

from ui_myform import Ui_myForm
from PyQt4.QtGui import QDialog

class MyForm(QDialog, Ui_myForm):

    def __init__(self, parent = None):
        QDialog.__init__(self, parent)

        self.setupUi(self)    #here Ui_myForm creates all widgets as members 
                              #of this object.
                              #now you can access every widget defined in 
                              #myForm as attributes of self   

        #supposing you defined two pushbuttons on your .UI file:
        self.pushButtonB.setEnabled(False)

        #you can connect signals of the generated widgets
        self.pushButtonA.clicked.connect(self.pushButtonAClicked)



    def bucar_actualizaciones(self):
        self.pushButtonB.setEnabled(True)

The names of the widgets are the one you set on QtDesigner, but is easy to inspect ui_myform.py in order to see the available widgets and names.

In order to use a custom widget in QtDesigner, you can right-click the button, and go to Promote to.... There you'l have to enter:

  • Base class name: QPushButton for example
  • Promoted class name: MyPushButton (this must be the name of the class of your custom widget)
  • Header file: mypushbutton.h. This will be converted to .py by pyuic.

Click Add and then Promote.

When you run pyuic, it will add this line at the end of ui_myform.py

from mypushbutton import MyPushButton

Also, you'll see that the generated code used MyPushButton instead of QPushButton

Smasho
  • 1,170
  • 8
  • 19
  • `self.btn_draggable.setDragDropMode(QtGui.QAbstractItemView.DragDrop)` I tried to set that property to a Button in that __init__ method. Obviously didn't work: `Exception "unhandled AttributeError" 'QPushButton' object has no attribute 'setDragDropMode'` Now, I'm not sure if custom properties or behaviour can be done in this way. Maybe I need to reimplement by pure code instead using designer :S – Mr_LinDowsMac Feb 10 '15 at 05:32
  • QPushButtons doesn't have a setDragDropMode() attribute. Whether they are created by you or by QtDesigner-pyuic. I edited the post to show how to use custom widgets. You'll have to create your own DragableButton, and make it dragable. Check [this](http://stackoverflow.com/questions/14395799/pyqt4-drag-and-drop) – Smasho Feb 10 '15 at 13:22
  • I compiled with pyuic4 the ui file, and I see the imported promoted widget. However, when I run the file which runs the application, I got an ImportError: `ImportError: No module named dragbutton` Do I have to import it also in the "main" file or I have to do something with pyuic4? – Mr_LinDowsMac Feb 10 '15 at 23:32
  • You have to create a file named dragbutton.py, with your custom widget inside (your custom DragButton which inherits QPushButton, must be defined inside *dragbutton.py*) – Smasho Feb 11 '15 at 00:11
1

My feeling is that you could try to achieve this with standard QWidget, but it would be easier to use QGraphicsScene/QGraphicsView API.

Also, note that you can embed a QWidget in a QGraphicsScene, using QGraphicsProxyWidget.

azf
  • 2,179
  • 16
  • 22
0

If you want to draw a line between the buttons, it means you need to reimplement the "paintEvent" of the background widget (Possibly the parent widget of all subwidgets), as you said, this is NOT the best practice to do so. Instead, you need to use QGraphicsWidget, for drawing line, you need to use QGraphicsLineItem. It have the following member functions:

setAcceptDrops
dragEnterEvent ( QGraphicsSceneDragDropEvent * )
dragLeaveEvent ( QGraphicsSceneDragDropEvent * )
dragMoveEvent ( QGraphicsSceneDragDropEvent * )

In the PyQt4 installation folder, there should exist a folder naming examples\graphicsview\diagramscene, which you take it for a reference.

Cui Heng
  • 1,265
  • 8
  • 10
0

You need to use a QDropEvent, I know this is not a very good answer but just create a QDropEvent function and in that function check the dropped button.

If firstButton's dropped on secondButton, painter->drawLine(firstButton.pos(), secondButton.pos()); You can use other points for drawing the line. Or you can use event->source() for the dragged button. You might need to define a QPen with some settings. When I say other points to use, I mean like firstButton.boundingRect().topRight().x(), firstButton.boundingRect.bottomRight().y() - firstButton.boundingRect.height() / 2

See: http://doc.qt.io/qt-5/qdropevent.html

Sorry, this code is pseudo C++ code, but you can easily adapt it to Python.

Example:

 void MainWindow::dropEvent(QDropEvent *event)
 {
     if(event->pos() == somePoint) //somePoint should be inside target's boundingRect, you need to write basic collision detection
          painter->drawLine(event->source().pos(), event->pos()); //You might use other points
 }

There are other drag and drop events also. You can change the target's color for example if dragged over it. See http://doc.qt.io/qt-5/dnd.html

I can try to help with collision detection code if you need it. I don't know if there's better way though. There may be. I'm mostly a C++ coder, but I can provide basic examples.

And the dropdown menu. You can create a simple QWidget with your menu with some mouseEvents and make it a child of the button, and set the y position of it so it shows under the button. For example (Again, C++, sorry):

 dropDownMenu->setParent(someButton);
 dropDownMenu->setPos(someButton.x(), someButton.y() + someButton.boundingRect().height());

You can hide or show it with mouseReleaseEvent. Just make sure to hide it back in your dropEvent function, so when you drag and drop it, it doesn't show.

Edit: Let me show you a simple collision detection code in C++, but easily adaptable to Python.

 if(event->pos() > target.boundingRect().topLeft().x() && event->pos() < target.topRight.x() && event->pos() > target.boundingRect().topRight() && event->pos() < target.boundingRect().bottomRight()) {
      //It's on the button.
 }

If you want a simpler solution. Just subclass the buttons you're going to drop onto and add the drag and drop events in their class. This would be easier, and shorter. I think dropEvent should work in a subclass too. I haven't tried it.

Edit: If you're asking how to do all of them using only Qt Designer, you can't. You need to write some code. You can't develop programs with Qt Designer, it's just for making the ui more easily. You can make software without Qt Designer, but you can't make software with only Qt Designer. You'll need to learn a bit of Python programming and a bit of PyQt for tasks like this. But both Python and Qt are easy enough to get the hang of them in a short time, and Qt documentation is marvellous.

Good luck!

  • See the edit in the end of the answer. It might be your real answer. –  Feb 06 '15 at 16:46
  • I already realize that can't make this enterly with Qt designer. My question is also about combining Qt designer by using the ui generated file and implement the custom widgets code with it. – Mr_LinDowsMac Feb 07 '15 at 00:37
  • I already gave you a thorough answer. I don't know any more than that, sorry. –  Feb 07 '15 at 00:38
  • Ok, so according to what you said I have to make this entirely by code :/ – Mr_LinDowsMac Feb 07 '15 at 00:43
  • That' right. Because AFAIK, you can't define events in the ui. You can connect some signals to slots, but I don't know if events can be treated like signals. Think of it like mouseEvents. –  Feb 07 '15 at 00:45
  • But, there is a but, you may call the signals of buttons to show and hide the drop down menus, but still you need to write a slot function to show and hide them. –  Feb 07 '15 at 00:50
  • Drop down menus might be implemented in Designer maybe. But drag and drop is an event. So, no luck there. –  Feb 07 '15 at 00:52
  • If you're a beginner, I'd say don't be afraid. Qt and Python are super easy to learn. What you want is easily doable. Even by a beginner, given he wants to learn a bit. –  Feb 07 '15 at 00:56
  • I was thinking about just using drop-down menus without drag n drop, but I still have to write the draw line event, right? – Mr_LinDowsMac Feb 07 '15 at 01:04
  • Yes, you'd need to write the draw line part. But believe me, it's not that hard. And besides you are on SO, you can ask your questions and get answers. What more do you want? –  Feb 07 '15 at 01:06
  • The code is not that cryptic when you start to understand it. It actually has a strict logic. –  Feb 07 '15 at 01:09
  • The code I wrote for drawing the line may have seemed complex. But actually it's not. What I wrote was calculating the middle of the right side of the button. It would make perfect sense if you knew just a little more. –  Feb 07 '15 at 01:16
  • Yes, I guess so. I thought that using designer was the best approach to quickly develop an interface that have a lot of controls. – Mr_LinDowsMac Feb 07 '15 at 01:16
  • Well, time to read "Rapid Programming GUI with Python and Qt" book xD – Mr_LinDowsMac Feb 07 '15 at 01:18
  • I wish you good luck, and happy learning. Because it gets better when you start to achieve stuff. It's actually a lot of fun. You just need to cross a barrier, that's all. –  Feb 07 '15 at 01:19
  • 1
    @Mr_LinDowsMac, **you can** combine the QtDesigner form with your own code. The way of doing it with PyQt is subclasing the class generated by pyuic tool – Smasho Feb 09 '15 at 21:02
  • @Smasho He asks if he can do it **without** writing code. –  Feb 09 '15 at 21:13
  • 1
    I agree, there is no way of doing this entirely in QtDesigner. Subclassing the form generated you just get the GUI you defined, without behavior (the most behavior you can get is some signal-slots connections) – Smasho Feb 09 '15 at 21:19
  • Actually that's the way that I wanted from the beginning, perhaps by subclassing as @Smasho suggested, but I don't know how to it properly, or at least don't know how to implement custom behaviour in the widgets of a pyuic generated file. – Mr_LinDowsMac Feb 10 '15 at 02:06