On Supermarket Cuts and Dynamic CSS in Qt Apps

Posted in Coding | Projects | Qt 0

I always prefer to buy whole or large cuts of fish over the pre-made steaks and filets at the grocers.  Living on the Gulf Coast makes this easy, I can always head down to the piers at Galveston, and find something nice at the fish markets.  (Or, just take the kayak out and come home with very fresh redfish, flounder, or trout from the marshes.) The reason is simple: I pay bulk prices and then can choose how I want to prepare the meat.  The grocer only occasionally has the cuts I want – imagine the look on the barely trained fishmongers face when you ask her what she did with all of the saltwater quail?  I mean, I know those redfish filets came from somewhere…

In the same vein, it’s important to give your power users choice.  Like the grocer who doesn’t impress me when they charge $25/lb for a yellowfin steak that’s half sinew, your design decisions may be perfect for most users, but give someone who uses your app every day the shivers. Once you make a decision to use cascading style sheets in your Qt application, why not let your users have some control over them?  Sure, most won’t touch the CSS themes, but some may – and a contributing user is the best kind you can have. Imagine that you could have a color-blind theme with no additional effort, or someone could contribute day/night themes.

We take a bit of load-time hit to use dynamic CSS themes stored on-disk, but to be fair – on most modern systems it’s not going to be noticeable to the user. When I built Graffik, I chose to make themes organized into a folder inside of the application folder, and make them user-editable. The user can also change the theme while the app is running, and choose which theme looks best for them.  I’ve broken out the QtDynamicThemer class which enables this separately, to make it easier to find and use, but it’s pretty straight-forward:

One-time themeing (for short-lived items like dialogs) – we can just set a dynamic style sheet from the ctor:

ErrorDialog::ErrorDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ErrorDialog) {
     ui->setupUi(this);
     setStyleSheet(SingleThemer::getStyleSheet("error"));
}

Note that getStyleSheet is static, so we don’t need to hold onto an object instance.

Or, we can use signals and slots to handle theme updates for long-lived objects like MainWindows and other widgets that are persistent:

LongLivedWidget(QWidget *parent) : QWidget(parent), ui(new Ui::LongLivedWidget) {
ui->setupUi(this);

    // theming

    Themer* theme = &Singleton::Instance();

    // watch for changes to the current theme
    connect(theme, SIGNAL(themeChanged()), this, SLOT(_themeChanged()));

    setStyleSheet(theme->getThemeCSS("longwidget"));
}

void LongLivedWidget::_themeChanged() {
    setStyleSheet(SingleThemer::getStyleSheet("longwidget"));
    Themer::rePolish(this);
}

(Of course, we use a singleton as the class needs to read the disk to see what themes are available. In this case, a singleton lets us read the disk once at startup for all instances.)

Using the Themer Class

The core Themer class expects a directory in the application path, which you can specify at compile (or run) time via the themePath() method. Each named theme will exist as a subdirectory within the theme directory, with the directory name being the same as the theme name. For example:

themes/
  Theme1/
  Theme2/

Within each directory lives the theme files, with one file for each named object. A named object is any independent theme setup you want to create, you can do this on a per-widget basis (“fooWidget“), or on a whole class of widgets – it’s up to you. The name of the object will be passed to the getStyleSheet() method. You can specify OS-specific theme files as well, see the docs for more information on this. A normal setup might look like:

themes/
    Theme1/
       foo.css
       foo_win.css
       bar.css
       bar_osx.css
       bar_win.css

Automating Theme File Deployment in Your Project File

Of course, having a bunch of theme files to copy around can be a pain in the butt at build and deployment time. I like to use a project include file to automate this. Essentially, create a subdirectory in your project to hold all of the theme files, and then add a .pri file in there that uses a QMAKE_EXTRA_TARGET directive to copy the files to the correct location in your build/deploy process. Take a look at Graffik’s themes.pri file for an example of one way to do this.

That’s it – it’s a simple class, with simple usage, but by combining it with a CSS-styled application, you can give your users more control, and easily provide varying themes yourself. Not to mention, it’ll let your designers skin your application without mucking with your classes. *wink*

Download or fork QtDynamicThemer on GitHub.

Leave a Reply