PDA

View Full Version : Create a dialog on the fly dynamically based on an input xml file



franco.amato
25th November 2021, 01:12
Hi Qt community,

I need to read an xml custom file and based on its content I have to create and show a dialog showing what has been described in the xml file.
A small example of part of the content of the file:


<?xml version="1.0" encoding="UTF-8"?>
<HMI>
<Parameter type="textfield">
<Label>DS ID</Label>
<Size>80</Size>
<Value>-1</Value>
<ToolTip>Enter DS_ID. For all use -1</ToolTip>
</Parameter>
<Parameter type="textfield">
<Label>RT Number</Label>
<Size>80</Size>
<Value>-1</Value>
<ToolTip>Track number. For all use -1</ToolTip>
</Parameter>
<Parameter type="textfield">
<Label>Sigma Value</Label>
<Size>80</Size>
<Value>2</Value>
<ToolTip>Sigma value. For for which AA will be performed</ToolTip>
</Parameter>
</HMI>


This will be translated in a dialog having 3 couples of QLabel and QLineEdit respectively.
Each couple of <Label>labelText</Label> tags identifies the QLabel (having labelText as text) and each Parameter type="textfield" identifies the QLineEdit widgets with the text between the tags <Value> and </Value>. I have no problem in parsing an xml file, but I'm not very clear on how to create dialog on the fly.

Any help will be appreciated.
Franco

Ginsengelf
25th November 2021, 13:45
Hi, I would try it like this:
- create a new widget with a QGridLayout
- loop over all labels, create a new QLabel for each label, and put it into first column of the layout
- loop over all lineedits, create a new QLineEdit for each lineedit, and put it into the second column of the layout

Hope this helps,

Ginsengelf

d_stranz
25th November 2021, 17:21
Adding to Ginsengelf's reply, I have a very simple helper class derived from QDialog which allows hosting of any widget inside of a QDialog. I create the widget first, hook up all of its signals, then add it to the helper dialog class and call exec(). This saves me the work of having to repeat the same code over and over for every custom dialog, and allows me to use the same widgets in other, non-dialog parts of the UI. Maybe this can help in your case, especially if you devise a custom QWidget class that builds itself from XML and provides a generic set of signals that are emitted when things are edited.

I assume you realize that Qt's UI files are just fancier versions of the XML file that you have devised, right? And that there is a QUiLoader class that will load and build a GUI from a UI file at runtime?



/*
QtWidgetHostingDialog

This is a generic QDialog class designed to host a single composite widget.
The dialog has a vertical layout with the hosted widget on top and a
standard QDialogButtonBox below. The button box's accepted and rejected
signals are connected to the QDialog accept and reject slots.

The pointer to the hosted widget is passed during construction and
ownership is passed to the QDialog. The hosted widget may not be replaced
once the dialog is constructed.

Convenience methods are provided to retrieve the hosted widget and button box
pointers.

Example:

MyWidget * pWidget = new MyWidget;
// configure pWidget, connect signals, etc.

CQtWidgetHostingDialog dlg( pWidget, this );
dlg.setWindowTitle( "Edit My Parameters" );
if ( QDialog::Accepted == dlg.exec() )
// retrieve edited information from pWidget

*/

#include <QDialog>

class QDialogButtonBox;

class CQtWidgetHostingDialog : public QDialog
{
Q_OBJECT

public:
CQtWidgetHostingDialog( QWidget * pHostedWidget, QWidget * pParent = nullptr );
virtual ~CSAQtWidgetHostingDialog();

QWidget * hostedWidget() const;
QDialogButtonBox * buttonBox() const;

protected:
QWidget * mpHostedWidget = nullptr;
QDialogButtonBox * mpButtonBox = nullptr;
};




#include "QtWidgetHostingDialog.h"

#include <QDialogButtonBox>
#include <QVBoxLayout>

#include <cassert>

CQtWidgetHostingDialog::CQtWidgetHostingDialog( QWidget * pHostedWidget, QWidget * pParent /*= nullptr */ )
: QDialog( pParent )
, mpHostedWidget( pHostedWidget )
{
assert( mpHostedWidget != nullptr );

QVBoxLayout * pLayout = new QVBoxLayout;
setLayout( pLayout );

pLayout->addWidget( mpHostedWidget );

mpButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
pLayout->addWidget( mpButtonBox );

connect( mpButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
connect( mpButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
}

CQtWidgetHostingDialog::~CQtWidgetHostingDialog()
{
}

QWidget * CQtWidgetHostingDialog::hostedWidget() const
{
return mpHostedWidget;
}

QDialogButtonBox * CQtWidgetHostingDialog::buttonBox() const
{
return mpButtonBox;
}


Access to the QDialogButtonBox is provided so you can customize by adding additional buttons and connect to their signals.

franco.amato
27th November 2021, 22:53
void ReportParametersDialog::processXmlFile(const QString &xmlFile, QGridLayout *layout)
{
QDomDocument document("parameters");
QFile file(xmlFile);
if (!file.open(QIODevice::ReadOnly))
{
return;
}
if (!document.setContent(&file))
{
file.close();
return;
}
QDomElement root = document.documentElement();
QDomNode node = root.firstChild();
while (!node.isNull())
{
QDomElement param = node.toElement();
if (!param.isNull())
{
if (param.tagName() != "Parameter")
{
continue;
}
if (param.attribute("type") == "textfield")
{
QDomElement labelElem = node.firstChildElement("Label");
QLabel *label = new QLabel(labelElem.text());
QDomElement value = node.firstChildElement("Value");
QDomElement size = node.firstChildElement("Size");
QDomElement toolTip = node.firstChildElement("ToolTip");

QLineEdit *lineEdit = new QLineEdit(value.text());
lineEdit->setMaximumWidth(size.text().toInt());
lineEdit->setToolTip(toolTip.text());

int row = layout->rowCount();
layout->addWidget(label, row, 0);
layout->addWidget(lineEdit, row, 1);
}
}
node = node.nextSibling(); }
}

I thought about using QSignalMapper but I don't see how to detect if the value of any widget has changed without storing the value somewhere.
Do you have any better idea?
Regards,
Franco

d_stranz
28th November 2021, 02:01
In your loop that creates the QLineEdit, you can use the QObject::setProperty() method to associate a unique value with the widget (as a QVariant). You can also connect the QLineEdit::editingFinished() signal to a slot. In the slot, call sender() to retrieve the QLineEdit pointer, then use the QObject::property() method to retrieve the unique value you have stored for the widget. You can also use QLineEdit::text() to retrieve whatever the user has changed the text to.

You are going to have devise some way of connecting the widgets you create via XML to fields in a data structure, otherwise you will end up with widgets connected to slots that don't have any idea what to do with the data they have retrieved.

But there are a lot of problems with your design. How are you going to prevent users from entering incorrect values, for example? How will you enforce required fields (i.e. a field that cannot be left empty)? How can you set the tab order so that the tab key doesn't just take the user to random places in the grid each time it is pressed? What you will be creating is a dialog with no way to control what happens between the time the dialog is displayed until the time the user clicks OK or Cancel.

franco.amato
28th November 2021, 02:15
Sorry d_stratz,
it seems that some text has been lost in my previous answer.

Regarding what you said, so far I am assuming that the xml has correct values.
But you are right, I will follow your advice and do the checks at the moment of parsing the file.

To replace the edited value of the QLineEdit in the xml, I was thinking of using the position in the grid of the widget (basically the row number) and associating it with the position of the tag in the xml file. What do you think?

d_stranz
28th November 2021, 15:54
To replace the edited value of the QLineEdit in the xml, I was thinking of using the position in the grid of the widget (basically the row number) and associating it with the position of the tag in the xml file. What do you think?

I think it would be better to add a new field to the XML element - a "key" or "id" that you can use as the unique property the you can assign to the QLineEdit. When the edited value changes, you get the property from the widget, find the same key or id in your XML, and use that to change the value.

Using the location of the widget in the grid to find it in the XML is fragile - if you add a new widget or change the order of the widgets in the XML, then the mapping between the grid and the XML is no longer correct. Adding a "key" that uniquely maps between widget and XML makes your layout independent of the number and placement of the widgets in the XML.

To make it easy, you could use a QMap< QString, QDomElement * > data structure to give you a quick way to look up the XML element that gets changed whenever the QLineEdit with that key (the QString):



void MyDialog::onEditingFinished()
{
QLineEdit * pEdit = qobject_cast< QLineEdit * >( sender() );
if ( pEdit )
{
QString text = pEdit->text();
QString key = pEdit->property( "key" ).toString();

// QMap< QString, QDomElement * > mKeyMap is a member of MyDialog
QDomElement * pXML = mKeyMap[ key ];
pXML->setAttribute( "myAttr", text );
}
}


You build the QMap when you read the XML to create the GUI. And of course you would add error checking to the code above to make sure that the "key" exists in the map, and that the map lookup has returned a non-null pointer.

franco.amato
29th November 2021, 12:51
Yours is a good idea, but unfortunately I can't change the xml files.
However, giving a look at the xml, I noticed that the label names are unique and I can add these names as properties of the QLineEdit widgets

QLineEdit::setProperty("id", labelName);

Then, I can use the labelName as information to retrieve the correct modified tag (even if I don't know how yet). It is the first time that I work with xml files