mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-03 16:27:46 +08:00
Database: added "parameters" field in Statistics table. GUI: detecting if parameters in database are different from the Preferences, if so ask user to update them.
This commit is contained in:
@@ -88,7 +88,7 @@ public:
|
||||
void updateLink(const Link & link);
|
||||
|
||||
public:
|
||||
void addStatisticsAfterRun(int stMemSize, int lastSignAdded, int processMemUsed, int databaseMemUsed, int dictionarySize) const;
|
||||
void addStatisticsAfterRun(int stMemSize, int lastSignAdded, int processMemUsed, int databaseMemUsed, int dictionarySize, const ParametersMap & parameters) const;
|
||||
|
||||
public:
|
||||
// Mutex-protected methods of abstract versions below
|
||||
@@ -107,6 +107,7 @@ public:
|
||||
int getLastDictionarySize() const; // working memory
|
||||
int getTotalNodesSize() const;
|
||||
int getTotalDictionarySize() const;
|
||||
ParametersMap getLastParameters() const;
|
||||
|
||||
void executeNoResult(const std::string & sql) const;
|
||||
|
||||
@@ -149,6 +150,7 @@ private:
|
||||
virtual int getLastDictionarySizeQuery() const = 0;
|
||||
virtual int getTotalNodesSizeQuery() const = 0;
|
||||
virtual int getTotalDictionarySizeQuery() const = 0;
|
||||
virtual ParametersMap getLastParametersQuery() const = 0;
|
||||
|
||||
virtual void executeNoResultQuery(const std::string & sql) const = 0;
|
||||
|
||||
|
||||
@@ -495,6 +495,9 @@ public:
|
||||
static std::string getVersion();
|
||||
static std::string getDefaultDatabaseName();
|
||||
|
||||
static std::string serialize(const ParametersMap & parameters);
|
||||
static ParametersMap deserialize(const std::string & parameters);
|
||||
|
||||
static bool isFeatureParameter(const std::string & param);
|
||||
static ParametersMap getDefaultOdometryParameters(bool stereo = false);
|
||||
static ParametersMap getDefaultParameters(const std::string & group);
|
||||
|
||||
@@ -188,6 +188,14 @@ int DBDriver::getTotalDictionarySize() const
|
||||
_dbSafeAccessMutex.unlock();
|
||||
return words;
|
||||
}
|
||||
ParametersMap DBDriver::getLastParameters() const
|
||||
{
|
||||
ParametersMap parameters;
|
||||
_dbSafeAccessMutex.lock();
|
||||
parameters = getLastParametersQuery();
|
||||
_dbSafeAccessMutex.unlock();
|
||||
return parameters;
|
||||
}
|
||||
|
||||
std::string DBDriver::getDatabaseVersion() const
|
||||
{
|
||||
@@ -834,18 +842,38 @@ void DBDriver::getAllLabels(std::map<int, std::string> & labels) const
|
||||
_dbSafeAccessMutex.unlock();
|
||||
}
|
||||
|
||||
void DBDriver::addStatisticsAfterRun(int stMemSize, int lastSignAdded, int processMemUsed, int databaseMemUsed, int dictionarySize) const
|
||||
void DBDriver::addStatisticsAfterRun(
|
||||
int stMemSize,
|
||||
int lastSignAdded,
|
||||
int processMemUsed,
|
||||
int databaseMemUsed,
|
||||
int dictionarySize,
|
||||
const ParametersMap & parameters) const
|
||||
{
|
||||
ULOGGER_DEBUG("");
|
||||
if(this->isConnected())
|
||||
{
|
||||
std::stringstream query;
|
||||
query << "INSERT INTO Statistics(STM_size,last_sign_added,process_mem_used,database_mem_used,dictionary_size) values("
|
||||
<< stMemSize << ","
|
||||
<< lastSignAdded << ","
|
||||
<< processMemUsed << ","
|
||||
<< databaseMemUsed << ","
|
||||
<< dictionarySize << ");";
|
||||
if(uStrNumCmp(this->getDatabaseVersion(), "0.11.8") >= 0)
|
||||
{
|
||||
std::string param = Parameters::serialize(parameters);
|
||||
query << "INSERT INTO Statistics(STM_size,last_sign_added,process_mem_used,database_mem_used,dictionary_size,parameters) values("
|
||||
<< stMemSize << ","
|
||||
<< lastSignAdded << ","
|
||||
<< processMemUsed << ","
|
||||
<< databaseMemUsed << ","
|
||||
<< dictionarySize << ","
|
||||
"\"" << param.c_str() << "\");";
|
||||
}
|
||||
else
|
||||
{
|
||||
query << "INSERT INTO Statistics(STM_size,last_sign_added,process_mem_used,database_mem_used,dictionary_size) values("
|
||||
<< stMemSize << ","
|
||||
<< lastSignAdded << ","
|
||||
<< processMemUsed << ","
|
||||
<< databaseMemUsed << ","
|
||||
<< dictionarySize << ");";
|
||||
}
|
||||
|
||||
this->executeNoResultQuery(query.str());
|
||||
}
|
||||
|
||||
@@ -699,6 +699,42 @@ int DBDriverSqlite3::getTotalDictionarySizeQuery() const
|
||||
return size;
|
||||
}
|
||||
|
||||
ParametersMap DBDriverSqlite3::getLastParametersQuery() const
|
||||
{
|
||||
UDEBUG("");
|
||||
ParametersMap parameters;
|
||||
if(_ppDb)
|
||||
{
|
||||
if(uStrNumCmp(_version, "0.11.8") >= 0)
|
||||
{
|
||||
std::string query = "SELECT parameters "
|
||||
"FROM Statistics "
|
||||
"WHERE time_enter >= (SELECT MAX(time_enter) FROM Statistics);";
|
||||
|
||||
int rc = SQLITE_OK;
|
||||
sqlite3_stmt * ppStmt = 0;
|
||||
rc = sqlite3_prepare_v2(_ppDb, query.c_str(), -1, &ppStmt, 0);
|
||||
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
|
||||
rc = sqlite3_step(ppStmt);
|
||||
if(rc == SQLITE_ROW)
|
||||
{
|
||||
std::string text((const char *)sqlite3_column_text(ppStmt, 0));
|
||||
|
||||
if(text.size())
|
||||
{
|
||||
parameters = Parameters::deserialize(text);
|
||||
}
|
||||
|
||||
rc = sqlite3_step(ppStmt);
|
||||
}
|
||||
UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
|
||||
rc = sqlite3_finalize(ppStmt);
|
||||
UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str());
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
void DBDriverSqlite3::loadNodeDataQuery(std::list<Signature *> & signatures) const
|
||||
{
|
||||
UDEBUG("load data for %d signatures", (int)signatures.size());
|
||||
|
||||
@@ -62,6 +62,7 @@ private:
|
||||
virtual int getLastDictionarySizeQuery() const;
|
||||
virtual int getTotalNodesSizeQuery() const;
|
||||
virtual int getTotalDictionarySizeQuery() const;
|
||||
virtual ParametersMap getLastParametersQuery() const;
|
||||
|
||||
virtual void executeNoResultQuery(const std::string & sql) const;
|
||||
|
||||
|
||||
@@ -1275,7 +1275,8 @@ void Memory::clear()
|
||||
_lastSignature?_lastSignature->id():0,
|
||||
UProcessInfo::getMemoryUsage(),
|
||||
_dbDriver->getMemoryUsed(),
|
||||
(int)_vwd->getVisualWords().size());
|
||||
(int)_vwd->getVisualWords().size(),
|
||||
parameters_);
|
||||
}
|
||||
}
|
||||
UDEBUG("");
|
||||
|
||||
@@ -82,6 +82,68 @@ std::string Parameters::getDefaultDatabaseName()
|
||||
return "rtabmap.db";
|
||||
}
|
||||
|
||||
std::string Parameters::serialize(const ParametersMap & parameters)
|
||||
{
|
||||
std::stringstream output;
|
||||
for(ParametersMap::const_iterator iter=parameters.begin(); iter!=parameters.end(); ++iter)
|
||||
{
|
||||
if(iter != parameters.begin())
|
||||
{
|
||||
output << ";";
|
||||
}
|
||||
// make sure there are no commas instead of dots
|
||||
output << iter->first << ":" << uReplaceChar(iter->second, ',', '.');
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
|
||||
ParametersMap Parameters::deserialize(const std::string & parameters)
|
||||
{
|
||||
ParametersMap output;
|
||||
std::list<std::string> tuplets = uSplit(parameters, ';');
|
||||
for(std::list<std::string>::iterator iter=tuplets.begin(); iter!=tuplets.end(); ++iter)
|
||||
{
|
||||
std::list<std::string> p = uSplit(*iter, ':');
|
||||
if(p.size() == 2)
|
||||
{
|
||||
std::string key = p.front();
|
||||
std::string value = p.back();
|
||||
|
||||
// look for old parameter name
|
||||
bool addParameter = true;
|
||||
std::map<std::string, std::pair<bool, std::string> >::const_iterator oldIter = Parameters::getRemovedParameters().find(key);
|
||||
if(oldIter!=Parameters::getRemovedParameters().end())
|
||||
{
|
||||
addParameter = oldIter->second.first;
|
||||
if(addParameter)
|
||||
{
|
||||
key = oldIter->second.second;
|
||||
UWARN("Parameter migration from \"%s\" to \"%s\" (value=%s).",
|
||||
oldIter->first.c_str(), oldIter->second.second.c_str(), value.c_str());
|
||||
}
|
||||
else if(oldIter->second.second.empty())
|
||||
{
|
||||
UWARN("Parameter \"%s\" doesn't exist anymore.",
|
||||
oldIter->first.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
UWARN("Parameter \"%s\" doesn't exist anymore, you may want to use this similar parameter \"%s\":\"%s\".",
|
||||
oldIter->first.c_str(), oldIter->second.second.c_str(), Parameters::getDescription(oldIter->second.second).c_str());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(Parameters::getDefaultParameters().find(key) == Parameters::getDefaultParameters().end())
|
||||
{
|
||||
UWARN("Unknown parameter \"%s\"=\"%s\"! The parameter is still added to output map.", key.c_str(), value.c_str());
|
||||
}
|
||||
uInsert(output, ParametersPair(key, value));
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
bool Parameters::isFeatureParameter(const std::string & parameter)
|
||||
{
|
||||
std::string group = uSplit(parameter, '/').front();
|
||||
|
||||
@@ -205,6 +205,7 @@ void RtabmapThread::mainLoop()
|
||||
int id = 0;
|
||||
cv::Mat userData;
|
||||
UTimer timer;
|
||||
std::string str;
|
||||
switch(state)
|
||||
{
|
||||
case kStateDetecting:
|
||||
@@ -212,12 +213,14 @@ void RtabmapThread::mainLoop()
|
||||
break;
|
||||
case kStateInit:
|
||||
UASSERT(!parameters.at("RtabmapThread/DatabasePath").empty());
|
||||
str = parameters.at("RtabmapThread/DatabasePath");
|
||||
parameters.erase("RtabmapThread/DatabasePath");
|
||||
Parameters::parse(parameters, Parameters::kRtabmapImageBufferSize(), _dataBufferMaxSize);
|
||||
Parameters::parse(parameters, Parameters::kRtabmapDetectionRate(), _rate);
|
||||
Parameters::parse(parameters, Parameters::kRtabmapCreateIntermediateNodes(), _createIntermediateNodes);
|
||||
UASSERT(_dataBufferMaxSize >= 0);
|
||||
UASSERT(_rate >= 0.0f);
|
||||
_rtabmap->init(parameters, parameters.at("RtabmapThread/DatabasePath"));
|
||||
_rtabmap->init(parameters, str);
|
||||
break;
|
||||
case kStateChangingParameters:
|
||||
Parameters::parse(parameters, Parameters::kRtabmapImageBufferSize(), _dataBufferMaxSize);
|
||||
|
||||
@@ -82,6 +82,7 @@ CREATE TABLE Statistics (
|
||||
process_mem_used INTEGER,
|
||||
database_mem_used INTEGER,
|
||||
dictionary_size INTEGER,
|
||||
parameters TEXT,
|
||||
time_enter DATE
|
||||
);
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ public:
|
||||
QString loadCustomConfig(const QString & section, const QString & key);
|
||||
|
||||
rtabmap::ParametersMap getAllParameters() const;
|
||||
void setParameter(const std::string & key, const std::string & value);
|
||||
|
||||
//General panel
|
||||
int getGeneralLoggerLevel() const;
|
||||
@@ -305,8 +306,6 @@ protected:
|
||||
virtual void showEvent ( QShowEvent * event );
|
||||
virtual void closeEvent(QCloseEvent *event);
|
||||
|
||||
void setParameter(const std::string & key, const std::string & value);
|
||||
|
||||
virtual QString getParamMessage();
|
||||
|
||||
virtual void readGuiSettings(const QString & filePath = QString());
|
||||
|
||||
@@ -531,7 +531,18 @@ void DatabaseViewer::writeSettings()
|
||||
|
||||
settings.endGroup(); // DatabaseViewer
|
||||
|
||||
const ParametersMap & parameters = ui_->parameters_toolbox->getParameters();
|
||||
ParametersMap parameters = ui_->parameters_toolbox->getParameters();
|
||||
for(ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end();)
|
||||
{
|
||||
if(!ui_->parameters_toolbox->getParameterWidget(iter->first.c_str()))
|
||||
{
|
||||
parameters.erase(iter++);
|
||||
}
|
||||
else
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
Parameters::writeINI(path.toStdString(), parameters);
|
||||
|
||||
this->setWindowModified(false);
|
||||
@@ -588,6 +599,50 @@ bool DatabaseViewer::openDatabase(const QString & path)
|
||||
{
|
||||
pathDatabase_ = UDirectory::getDir(path.toStdString()).c_str();
|
||||
databaseFileName_ = UFile::getName(path.toStdString());
|
||||
|
||||
// look if there are saved parameters
|
||||
ParametersMap parameters = dbDriver_->getLastParameters();
|
||||
|
||||
if(parameters.size())
|
||||
{
|
||||
const ParametersMap & currentParameters = ui_->parameters_toolbox->getParameters();
|
||||
ParametersMap differentParameters;
|
||||
for(ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter)
|
||||
{
|
||||
ParametersMap::const_iterator jter = currentParameters.find(iter->first);
|
||||
if(jter!=currentParameters.end() &&
|
||||
ui_->parameters_toolbox->getParameterWidget(QString(iter->first.c_str())) != 0 &&
|
||||
iter->second.compare(jter->second) != 0 &&
|
||||
iter->first.compare(Parameters::kRtabmapWorkingDirectory()) != 0)
|
||||
{
|
||||
differentParameters.insert(*iter);
|
||||
QString msg = tr("Parameter \"%1\": database=\"%2\" Preferences=\"%3\"")
|
||||
.arg(iter->first.c_str())
|
||||
.arg(iter->second.c_str())
|
||||
.arg(jter->second.c_str());
|
||||
UWARN(msg.toStdString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if(differentParameters.size())
|
||||
{
|
||||
int r = QMessageBox::question(this,
|
||||
tr("Update parameters..."),
|
||||
tr("The database is using %1 different parameter(s) than "
|
||||
"those currently set in Core parameters panel. Do you want "
|
||||
"to use database's parameters?").arg(differentParameters.size()),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
if(r == QMessageBox::Yes)
|
||||
{
|
||||
for(rtabmap::ParametersMap::const_iterator iter = differentParameters.begin(); iter!=differentParameters.end(); ++iter)
|
||||
{
|
||||
ui_->parameters_toolbox->updateParameter(iter->first, iter->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateIds();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3597,6 +3597,56 @@ void MainWindow::openDatabase(const QString & path)
|
||||
|
||||
this->clearTheCache();
|
||||
_openedDatabasePath = path;
|
||||
|
||||
// look if there are saved parameters
|
||||
DBDriver * driver = DBDriver::create();
|
||||
if(driver->openConnection(value, false))
|
||||
{
|
||||
ParametersMap parameters = driver->getLastParameters();
|
||||
driver->closeConnection(false);
|
||||
delete driver;
|
||||
|
||||
if(parameters.size())
|
||||
{
|
||||
ParametersMap currentParameters = _preferencesDialog->getAllParameters();
|
||||
ParametersMap differentParameters;
|
||||
for(ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter)
|
||||
{
|
||||
ParametersMap::iterator jter = currentParameters.find(iter->first);
|
||||
if(jter!=currentParameters.end() &&
|
||||
iter->second.compare(jter->second) != 0 &&
|
||||
iter->first.compare(Parameters::kRtabmapWorkingDirectory()) != 0)
|
||||
{
|
||||
differentParameters.insert(*iter);
|
||||
QString msg = tr("Parameter \"%1\": database=\"%2\" Preferences=\"%3\"")
|
||||
.arg(iter->first.c_str())
|
||||
.arg(iter->second.c_str())
|
||||
.arg(jter->second.c_str());
|
||||
_ui->widget_console->appendMsg(msg);
|
||||
UWARN(msg.toStdString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if(differentParameters.size())
|
||||
{
|
||||
int r = QMessageBox::question(this,
|
||||
tr("Update parameters..."),
|
||||
tr("The database is using %1 different parameter(s) than "
|
||||
"those currently set in Preferences. Do you want "
|
||||
"to use database's parameters?").arg(differentParameters.size()),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
if(r == QMessageBox::Yes)
|
||||
{
|
||||
for(rtabmap::ParametersMap::const_iterator iter = differentParameters.begin(); iter!=differentParameters.end(); ++iter)
|
||||
{
|
||||
_preferencesDialog->setParameter(iter->first.c_str(), iter->second.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->post(new RtabmapEventCmd(RtabmapEventCmd::kCmdInit, value, 0, _preferencesDialog->getAllParameters()));
|
||||
applyPrefSettings(_preferencesDialog->getAllParameters(), false);
|
||||
}
|
||||
|
||||
@@ -249,43 +249,49 @@ void ParametersToolBox::updateParameter(const std::string & key, const std::stri
|
||||
QString group = QString::fromStdString(key).split("/").first();
|
||||
if(!ignoredGroups_.contains(group))
|
||||
{
|
||||
UASSERT_MSG(parameters_.find(key) != parameters_.end(), uFormat("key=\"%s\"", key.c_str()).c_str());
|
||||
parameters_.at(key) = value;
|
||||
QWidget * widget = this->findChild<QWidget*>(key.c_str());
|
||||
QString type = QString::fromStdString(Parameters::getType(key));
|
||||
if(type.compare("string") == 0)
|
||||
if(parameters_.find(key) == parameters_.end())
|
||||
{
|
||||
QString valueQt = QString::fromStdString(value);
|
||||
if(valueQt.contains(';'))
|
||||
UWARN("key=\"%s\" doesn't exist", key.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters_.at(key) = value;
|
||||
QWidget * widget = this->findChild<QWidget*>(key.c_str());
|
||||
QString type = QString::fromStdString(Parameters::getType(key));
|
||||
if(type.compare("string") == 0)
|
||||
{
|
||||
// It's a list, just change the index
|
||||
QStringList splitted = valueQt.split(':');
|
||||
((QComboBox*)widget)->setCurrentIndex(splitted.first().toInt());
|
||||
QString valueQt = QString::fromStdString(value);
|
||||
if(valueQt.contains(';'))
|
||||
{
|
||||
// It's a list, just change the index
|
||||
QStringList splitted = valueQt.split(':');
|
||||
((QComboBox*)widget)->setCurrentIndex(splitted.first().toInt());
|
||||
}
|
||||
else
|
||||
{
|
||||
((QLineEdit*)widget)->setText(valueQt);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if(type.compare("int") == 0)
|
||||
{
|
||||
((QLineEdit*)widget)->setText(valueQt);
|
||||
((QSpinBox*)widget)->setValue(uStr2Int(value));
|
||||
}
|
||||
else if(type.compare("uint") == 0)
|
||||
{
|
||||
((QSpinBox*)widget)->setValue(uStr2Int(value));
|
||||
}
|
||||
else if(type.compare("double") == 0)
|
||||
{
|
||||
((QDoubleSpinBox*)widget)->setValue(uStr2Double(value));
|
||||
}
|
||||
else if(type.compare("float") == 0)
|
||||
{
|
||||
((QDoubleSpinBox*)widget)->setValue(uStr2Float(value));
|
||||
}
|
||||
else if(type.compare("bool") == 0)
|
||||
{
|
||||
((QCheckBox*)widget)->setChecked(uStr2Bool(value));
|
||||
}
|
||||
}
|
||||
else if(type.compare("int") == 0)
|
||||
{
|
||||
((QSpinBox*)widget)->setValue(uStr2Int(value));
|
||||
}
|
||||
else if(type.compare("uint") == 0)
|
||||
{
|
||||
((QSpinBox*)widget)->setValue(uStr2Int(value));
|
||||
}
|
||||
else if(type.compare("double") == 0)
|
||||
{
|
||||
((QDoubleSpinBox*)widget)->setValue(uStr2Double(value));
|
||||
}
|
||||
else if(type.compare("float") == 0)
|
||||
{
|
||||
((QDoubleSpinBox*)widget)->setValue(uStr2Float(value));
|
||||
}
|
||||
else if(type.compare("bool") == 0)
|
||||
{
|
||||
((QCheckBox*)widget)->setChecked(uStr2Bool(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2679,7 +2679,6 @@ void PreferencesDialog::selectSourceDatabase()
|
||||
if(paths.size())
|
||||
{
|
||||
int r = QMessageBox::question(this, tr("Odometry in database..."), tr("Use odometry saved in database (if some saved)?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
|
||||
_ui->source_checkBox_ignoreOdometry->setChecked(r != QMessageBox::Yes);
|
||||
_ui->source_database_lineEdit_path->setText(paths.size()==1?paths.front():paths.join(";"));
|
||||
_ui->source_spinBox_databaseStartPos->setValue(0);
|
||||
@@ -2918,10 +2917,11 @@ void PreferencesDialog::selectSourceSvoPath()
|
||||
void PreferencesDialog::setParameter(const std::string & key, const std::string & value)
|
||||
{
|
||||
UDEBUG("%s=%s", key.c_str(), value.c_str());
|
||||
uInsert(_parameters, ParametersPair(key, value));
|
||||
QWidget * obj = _ui->stackedWidget->findChild<QWidget*>(key.c_str());
|
||||
if(obj)
|
||||
{
|
||||
uInsert(_parameters, ParametersPair(key, value));
|
||||
|
||||
QSpinBox * spin = qobject_cast<QSpinBox *>(obj);
|
||||
QDoubleSpinBox * doubleSpin = qobject_cast<QDoubleSpinBox *>(obj);
|
||||
QComboBox * combo = qobject_cast<QComboBox *>(obj);
|
||||
|
||||
Reference in New Issue
Block a user