Platform-native preferences dialog with Qt on Mac,

2019-04-12 12:37发布

问题:

On Mac and Gnome, native applications use an application preferences dialog that immediately applies the chosen settings as they are selected. On Windows and (I think) KDE, preferences are applied only when an "Apply" or "OK" button is pressed.

Are there any built-in Qt goodies to do this for you, or do you have to include several #ifdef's in the dialog code to handle this (Q_WS_WIN, Q_WS_MAC, Q_WS_X11)?

If you have done something like this before (even using #ifdef's), could you share skeleton code as to how you pulled it off?

回答1:

Looks like you have to spin your own. Here are the important parts of our solution. This could probably be generalized if someone is so inclined. I'm also able to assume certain things because of our business rules that may break other applications. The toggle is a macro that can be defined at compile-time: YOUR_APP_APPLY_PREFERENCES_IMMEDIATELY

preferences_dialog.h

class PreferencesDialog : public QDialog {
  Q_OBJECT

public:
  explicit PreferencesDialog(... various arguments ...,
                             QWidget *parent);
private slots:
  void ModifyMapLanguages();

private:
  void ApplyLanguageChanges(const QList<Language> &languages,
                            const QHash<LanguageKey, LanguageKey> &renames);
  void Initialize();

#ifndef YOUR_APP_APPLY_PREFERENCES_IMMEDIATELY
public slots:
  void accept();
private slots:
  void ApplyDialog();
private:
  QList<Language> pending_languages_;
  QHash<LanguageKey, LanguageKey> pending_language_renames_;
#endif
};

preferences_dialog.cpp

#include "forms/preferences_dialog.h"
#include "ui_preferences_dialog.h"

PreferencesDialog::PreferencesDialog(... various arguments ...,
                                     QWidget *parent) :
    QDialog(parent),
    ... various initializers ... {
  ui->setupUi(this);
  Initialize();
}

void PreferencesDialog::ApplyLanguageChanges(
    const QList<Language> &languages,
    const QHash<LanguageKey, LanguageKey> &renames) {
  // Do the actual changes here, whether immediate or postponed
}

void PreferencesDialog::Initialize() {
  // Disable the minimize and maximize buttons.
  Qt::WindowFlags flags = this->windowFlags();
  flags |= Qt::CustomizeWindowHint;
  flags &= ~Qt::WindowMinMaxButtonsHint;
  setWindowFlags(flags);

// buttons is the QDialogButtonBox with Ok, Cancel, and Apply buttons
#ifdef YOUR_APP_APPLY_PREFERENCES_IMMEDIATELY      
  ui->buttons->setVisible(false);
#else
  QPushButton *apply_button = ui->buttons->button(QDialogButtonBox::Apply);
  connect(apply_button, SIGNAL(clicked()), SLOT(ApplyDialog()));
#endif    
}

void PreferencesDialog::ModifyMapLanguages() {
  // Get the changes; in my case, they are coming from a dialog wizard
  LanguageSetupWizard wizard(map_->languages(), true, this);
  wizard.setWindowModality(Qt::WindowModal);
  if (QDialog::Accepted == wizard.exec()) {  
#ifdef YOUR_APP_APPLY_PREFERENCES_IMMEDIATELY
    ApplyLanguageChanges(wizard.languages(), wizard.language_renames());
#else
    pending_languages_ = wizard.languages();
    pending_language_renames_ = wizard.language_renames();
#endif
  }
}

#ifndef YOUR_APP_APPLY_PREFERENCES_IMMEDIATELY

void PreferencesDialog::ApplyDialog() {
  if (!pending_languages_.isEmpty()) {
    ApplyLanguageChanges(pending_languages_, pending_language_renames_);
  }
}

void PreferencesDialog::accept() {
  ApplyDialog();
  QDialog::accept();
}
#endif


回答2:

QSettings is a cross-platform abstraction that can be used for saving preferences, so hopefully that obviates the use of #IFDEFs

I also found that it has really good performance. So good, in fact, that I just installed an event listener and called save() upon every single event.

The form's class contains an event filter like this:

def __init__(self, *args, **kwargs):
    # ....
    self.installEventFilter(self)
    # ....

def eventFilter(self, obj, event):
    self.save()
    return False

And my save() method looks like this:

self.app.settings.setValue(self.state_key, self.header.saveState())
self.app.settings.setValue(self.geometry_key, self.header.saveGeometry())
self.app.settings.setValue("connect_timeout_spinBox_value", self.connect_timeout_spinBox.value())
self.app.settings.setValue("reveal_downloads_checkbox_checked", self.reveal_downloads_checkbox.checkState())

So, yes, you have to do some leg-work yourself to get your prefs saved immediately - but I didn't find it too arduous.

And although it (still) feels a bit ham-fisted to spray out save()s on every event, QSettings fine performance made this operation undetectable to the user.

Writing a small class which enumerates every widget on a form and saves/restores each of its properties in QSettings would also be neat. This might be appropriate if you have some some tens/hundreds of preferences. I'll post back here when I've done it.