PDA

View Full Version : [PyQT] QDataWidgetMapper and Custom Widget



Vincent Le Saux
5th February 2015, 10:52
Hi all,

I'm trying to implement a treeview for my application and a qdatawidgetmapper to map the model data into a properties widget to facilitate the modification of the model data. So far, everything works pretty well (thanks to the M. Summerfield books). I can use easily QLineEdit, QSpinBox and QComboBox widgets in the properties widget. Now, I would like to add a custom widget (a "color" widget) in this properties wigdet. Basically, this custom widget is a QLineEdit which can not be manually edited (readOnly mode activated) and a QPushButton that opens a QColorDialog instance when the button is clicked. Basically, I choose a color from the QColorDialog instance, and the name of the color is programmatically written into the QLineEdit. But I don't know how to do the mapping between the model and the properties widget (both in vizualization and edition). I tried few things I found on the internet, but nothing worked and I'm stucked (the examples on the internet are not numerous).

As an example would probably be clearer for you to understand my problemn, I've added a simple example in attachement of this message. The file contains several classes:
- ProjectNode : basic class which represents the node of my treeview. Contains 2 attributes used for the mapping onto the properties widget : name and color
- MyModel : custom model derived from QAbstractItemModel
- myTreeView : custom treeview
- PropertiesEditor : widget used to contain the data of the model (name and color)
- MyTreeWidget : widget that combine the myTreeView and my Properties Editor
- ColorWidget : custom widget evoked previously

So my problem is : how to map the data of a model onto a custom widget?

Any help would be greatly appreciated! Thanks in advance for your time

Vincent

anda_skoa
5th February 2015, 11:36
Your color widget needs a property that the widget mapper can write to and read from.
See http://doc.qt.io/qt-5/properties.html for properties in general.

Then you either mark it as USER or specify the property's name with the addMapping() overload that takes three arguments.

Cheers,
_

Vincent Le Saux
5th February 2015, 15:23
Thank you very much for your time and reactivity.

I tried what you just said by adding in the mapping section:

self.dataMapper.addMapping(self.colorWidget, 1, "color")

and the"color" property is defined in the ColorWidget class using the QtCore.pyqtProperty class according to:



def getColor(self):
return self._color

def setColor(self, newColor):
self._color = newColor
self.ldtColor.setText(newColor)
self.updateIcon()

color = pyqtProperty(str, getColor, setColor)



However, this does not seem to solve the modification of the model data. If I navigate through my treeview items, I always get the initial value (the new value is not stored in the model). How can I solve this problem? By emitting a signal when the setColor method is called? If so, how can the QDataWidgetMapper be aware of this?

Once again, thank you for your time.

Cheers,

anda_skoa
6th February 2015, 10:00
Your property might need a NOTIFY signal to indicate that the value has changed.

Cheers,
_

Vincent Le Saux
6th February 2015, 15:58
I modified the ColorWidget as follows:



class ColorWidget(QWidget):
sigDataChanged = pyqtSignal()
def __init__(self, color=None):
QWidget.__init__(self)

if color is not None:
self._color = color
else:
self._color = "#ff0000"
self.ldtColor = QLineEdit(self.color)
self.ldtColor.setReadOnly(True)
self.btnColor = QPushButton()
pixmap = QPixmap(24, 24)
pixmap.fill(QColor(self.color))
icon = QIcon(pixmap)
self.btnColor.setIcon(icon)
self.btnColor.setIconSize(QSize(24, 24))
self.btnColor.setFixedWidth(24)
self.btnColor.setFixedHeight(24)
layout = QHBoxLayout()
layout.addWidget(self.ldtColor)
layout.addWidget(self.btnColor)
self.setLayout(layout)
self.setContentsMargins(0, 0, 0, 0)
self.layout().setContentsMargins(0, 0, 0, 0)
self.btnColor.clicked.connect(self.changeColor)

def changeColor(self):
colorInit = QColor()
print(self._color)
colorInit.setNamedColor(self._color)
color = QColorDialog.getColor(colorInit, self, "Select a color")
if color.isValid():
self.ldtColor.setText(color.name())
self.setColor(color.name())

def updateIcon(self):
pixmap = QPixmap(24, 24)
newcolor = QColor()
newcolor.setNamedColor(self._color)
pixmap.fill(newcolor)
icon = QIcon(pixmap)
self.btnColor.setIcon(icon)

def getColor(self):
return self._color

def setColor(self, newColor):
self._color = newColor
self.ldtColor.setText(newColor)
self.updateIcon()
self.sigDataChanged.emit()

color = pyqtProperty(str, fget=getColor, fset=setColor, notify=sigDataChanged)


Basically, I just added a pyqtSignal that is emitted when the color has changed. However, this modification does not solve the problem: this modification is not stored into the model. If I read the documentation of pyqtProperty (http://pyqt.sourceforge.net/Docs/PyQt5/qt_properties.html), I can read:

notify – the optional unbound notify signal. It is ignored by Python.

What does it means: whatever its value, pyqt will not interpret it? Therefore, can I solve my problem?

Thanks for your time, it is greatly appreciated!

Cheers,

Vincent

Vincent Le Saux
10th February 2015, 09:50
Hi all,

I found a way to circumvent the problem by mapping the model to the QLineEdit widget of my custom but I do not know how to proceed.

The QLineEdit is set in readOnly mode only and its content is programmatically modified. What signal needs to be emitted to tell the QDataWidgetMapper the content has been modified? Because, when a model is mapped on a QLineEdit, everything is automatic and I need to understand hows it works to be able to reproduce its behaviour.

Cheers,

Vincent

anda_skoa
10th February 2015, 16:53
I played a bit around and what seems to work is to connect the colorwidget's change signal to the mapper's submit slot.

Cheers,
_

Vincent Le Saux
11th February 2015, 08:03
Could you be more precise or ideally write the piece of code you mentionned because I do not see what you mean.

Cheers,

Vincent

anda_skoa
11th February 2015, 09:19
I haven't done any PyQt programming myself, but maybe something like


self.dataMapper.addMapping(self.colorWidget, 1, "color")
self.colorWidget.sigDataChanged.connect(self.dataM apper.submit);


Cheers,
_

Vincent Le Saux
11th February 2015, 11:39
I did not manage to make it working properly. By changing the policy of the dataMapper to Manual, I get plenty of errors. But I may not use the method correctly. I just did something like:

self.dataMapper.submit()
but it is not working.

It tried few other things and I noticed something which appear very strange to me. When I change the color attribute of my colorWidget with the QColorDialog, I then write the name of the color in the QLineEdit and modify the icon of the QPushButton. If I change the selected object within my treeView, the change is not stored in the model (we already knew that). However, if I set the focus on the QLineEdit and I press manually enter (to force the widget to emit the returnPressed signal), then the color name is stored into the model (the setData method is called). I tried to reproduce programmaticaly this behaviour by changing the focus on the QLineEdit and by emitting manually the returnPressed signal, but it does not solve my problem. I still have to interact with my widget to force the setData method to be called. Strange isn't it? I start to feel really dumb. This problem should have a simple solution, but I don't find it.

In attachment the last version of the example (it may be clearer for you to directly test the code).

Any help would be highly welcomed!

Thanks in advance for your time.

Cheers,

Vincent

anda_skoa
11th February 2015, 14:13
So you connect to a local slot and that one calls submit(), should work as well.

Is the slot being invoked?

it definitely works in C++. I have a color editing widget and connect its signal to the datawidgetmapper's submit() slot and whenever I change color, the model is update accordingly.



#ifndef COLOREDITOR_H
#define COLOREDITOR_H

#include <QColor>
#include <QWidget>

class QLineEdit;

class ColorEditor : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)

public:
explicit ColorEditor(QWidget *parent = 0);

QColor color() const;

signals:
void colorChanged(const QColor &color);

public slots:
void setColor(const QColor &color);

private:
QColor m_color;
QLineEdit *m_lineEdit;

private slots:
void onButtonClicked();
};

#endif // COLOREDITOR_H



#include "coloreditor.h"

#include <QColorDialog>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>

ColorEditor::ColorEditor(QWidget *parent)
: QWidget(parent)
{
QHBoxLayout *hbox = new QHBoxLayout(this);

m_lineEdit = new QLineEdit(this);
hbox->addWidget(m_lineEdit);

QPushButton *button = new QPushButton("...", this);
hbox->addWidget(button);
connect(button, SIGNAL(clicked()), this, SLOT(onButtonClicked()));
}

QColor ColorEditor::color() const
{
return m_color;
}

void ColorEditor::setColor(const QColor &color)
{
if (color == m_color) {
return;
}

m_color = color;
m_lineEdit->setText(m_color.name());

emit colorChanged(color);
}

void ColorEditor::onButtonClicked()
{
const QColor color = QColorDialog::getColor(m_color, this);
if (!color.isValid()) {
return;
}

setColor(color);
}



ui->tableView->setModel(model);

QDataWidgetMapper *mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->addMapping(ui->name, 0);
mapper->addMapping(ui->color, 1, "color");
mapper->toFirst();

connect(ui->color, SIGNAL(colorChanged(QColor)), mapper, SLOT(submit()));


Cheers,
_

Vincent Le Saux
11th February 2015, 15:29
I finally managed to make it working! Your code is exactly the same as I tried before my last post and I did not manage to make it working because of a recursivity problem (the sigDataChanged signal was directly emitted in the setColor method).

I had to change a little bit the code :


class ColorWidget(QWidget):
sigDataChanged = pyqtSignal(str)
def __init__(self, color=None):
some python code here
self.btnColor.clicked.connect(self.changeColor)

def changeColor(self):
colorInit = QColor()
colorInit.setNamedColor(self._color)
color = QColorDialog.getColor(colorInit, self, "Select a color")
if color.isValid():
self.ldtColor.setText(color.name())
self.setColor(color.name())
self.sigDataChanged.emit(self._color)

def updateIcon(self):
pixmap = QPixmap(24, 24)
newcolor = QColor()
newcolor.setNamedColor(self._color)
pixmap.fill(newcolor)
icon = QIcon(pixmap)
self.btnColor.setIcon(icon)

def getColor(self):
return self._color

def setColor(self, newColor):
self._color = newColor
self.ldtColor.setText(newColor)
self.updateIcon()

color = pyqtProperty(str, fget=getColor, fset=setColor)

I just emit the sigDataChanged signal after the setColor method has been called. Finally, I update the model by connecting this signal to the submit slot of the QDataWidgetMapper in the appropriate PropertiesEditor class (as you mentionned).


self.colorWidget.sigDataChanged.connect(self.dataM apper.submit)


Problem solved. I close the thread.

Thank you very much anda_skoa for your time and help :).

Cheers,

Vincent

anda_skoa
12th February 2015, 08:06
I finally managed to make it working! Your code is exactly the same as I tried before my last post and I did not manage to make it working because of a recursivity problem (the sigDataChanged signal was directly emitted in the setColor method).

My guess would be that this is a problem caused by not checking for actual change.

I do that in setColor(), so the signal is only emitted if the color actually changes.

But maybe PyQt behaves differently in some other aspect.

Cheers,
_

Vincent Le Saux
12th February 2015, 15:51
You are definitely right! I just check for changes in the setColor method, and the recursivity problem disappears.

PyQt and Qt behaves exactly the same way on this aspect.

Once again, thank you!

Cheers,

Vincent