[solved] Need help for implementing themes support



  • Qt offers various ways of customizing widgets by subclassing the base classes and reimplementing the painting functions, and that's quite cool. But while looking in the examples provided in the documentation and elsewhere I've been unable to find a proper example of themes support implementation in an application. I don't mean simply customizing the look with different colors or images that you define in your code; instead I mean dynamically loading a custom theme from say an XML file (read as "any kind of settings storage implementation" - files, registry... doesn't matter) having all the settings and path to any images used (like colors, widget sizes, text size, background images, etc...)

    Does someone have any simple example to share that uses some dynamic theme loading from some settings file? I don't need anything too complicated, I just need some start to get in the right direction for implementing my own themes support in my applications.
    Also, any other suggestions about themes is greatly appreciated, so let me know any goods you know. And thank you very much in advance!

    p.s. I'm using Windows and Qt 5.1.1 but examples of previous versions of Qt or an OS-specific implementation is also accepted. As I said, I need to get into the right direction about this.



  • Hi, have you read the documentation of QStyle and Qt stylesheet ?



  • Yes I did, I'm also reading the QCommonStyle documentation, and I came across this example:
    http://qt-project.org/doc/qt-5.1/qtwidgets/widgets-styles.html
    In the example there's a very neat explanation of what is being customized, but the actual customizations are made in code: all the colors, all the image paths, anything is defined in code.
    What I'm looking for is an example of getting these customizations (well... most of them, like colors and image paths) from some settings file and applying them dynamically at runtime. Dynamically also means being able to select another theme and applying it immediately at runtime without restarting the application.



  • on a limited scale i've done the dynamic loading of stylesheets by implementing a
    changeTheme(bool day)

    on every window and calling it with a different stylesheet string (for day and night ui)

    my changeTheme sets the stylesheet with one of 2 compiled in strings for every sub-widget depending on the boolean

    but there is no reason why those string can't come out of a config file.

    so perhaps have a stylesheet config file (sorry no xml here :) ) could look like:

    day.style
    @mainwindow="color: white; ..."
    mainwindow.button1="color:white; font-family: arial; ..."@

    and then a "

    night.style
    @mainwindow="color: black; ..."
    mainwindow.button1="color:black; font-family: arial; ..."@

    and in your :

    @MyWindow::changeTheme(QString filename)
    {
    this->setStylesheet( ..read mainwindow from filename )
    this->button1.setStylesheet( ..read mainwindow.button1 from filename )
    }@

    perhaps you could even use introspection to automatically find matching widget names in the config file (with a fallback to class-name, super-classname..)

    i too am looking for a more 'integrated' framework for themes. so if you find one please reply here and i'll get an email.



  • Solution one: There is a commandline parameter you can give any Qt application to make it use a custom stylesheet. These are evaluated before your application receives these parameters, I think.

    Solution two:I have a folder "Styles" within my application folder, within that I store a lot of styles in qss format. Some of them bring along icons/pictures, so they are inside their own folders.

    This is how I look for them:
    @QStringList FindStyleSheets()
    {
    QStringList files = ListFiles("Styles");//this recursively list all files in the folder
    for(int i = files.count() - 1 ; i >= 0 ; i --)
    {
    if(files.at(i).endsWith(".qss", Qt::CaseInsensitive))
    files.replace(i, files.at(i).left(files.at(i).count() - QString(".qss").count()));
    else
    files.takeAt(i);
    }
    return(files);
    }@

    The results then fill a QComboBox:
    @QComboBox *boxStyles = new QComboBox(this);
    boxStyles->addItems(QStyleFactory::keys() << FindStyleSheets());//I do include the standard Qt styles, mainly Fusion@

    Once a style is selected in the combobox, I call this function:
    @void ChangeStyle()
    {
    if(boxStyles->currentText().isEmpty())
    {
    qApp->setStyle(nativeStyle);//I saved the initial style somewhere, so I can go back w/o trouble
    }
    else if(QStyleFactory::keys().contains(boxStyles->currentText(), Qt::CaseInsensitive))//actual style
    {
    qApp->setStyleSheet("");//you could probably combine styles and stylesheets
    qApp->setStyle(boxStyles->currentText());
    }
    else//stylesheet - this should be interesting for you
    {
    CustomStyles c = CustomStyles(QString("Styles") + "/" + boxStyles->currentText() + QString(".qss"));
    c.SetCustomStyleSheet(); //more detail on this will follow
    }
    }@

    The CustomStyles class is very simple:
    @class CustomStyles
    {
    public:
    CustomStyles(const QString &Name);
    QString GetName();
    void SetName(const QString &Name);
    void SetCustomStyleSheet();
    private:
    QString name;
    };@

    All the functions are one-liners, except "SetCustomStyleSheet()":
    @void CustomStyles::SetCustomStyleSheet()
    {
    FILE * in = NULL;//wrote this a long time ago, still was used to C back then - should be upgraded to exclusively use QFile
    QByteArray data;
    QString style;
    QFile f(name);
    if(name.isEmpty())
    {
    qApp->setStyleSheet("");
    return;
    }
    if(!f.exists())
    return;

    in = fopen&#40;name.TOLOCAL , "rb"&#41;;
    if(in != NULL)
    {
        f.open(in , QIODevice::ReadOnly);
        data = f.readAll();
        f.close();
        fclose(in); //read in the file
        style = QString::fromUtf8(data.constData() );
        style.replace("skin:" , name.left(name.lastIndexOf(QRegExp("[\\\\/]")) + 1) , Qt::CaseInsensitive);//this line I'll explain below
        qApp->setStyleSheet(style);
    }
    

    }@

    Basically this reads the content of a stylesheet (.qss file) and calls
    @qApp->setStyleSheet(content_of_qss_file);@

    I changed some things so the stylesheets paths to images can be dynamic, that's why all the paths inside start with "skin:" and are replaced by actual paths right before the stylesheet is set.

    This is a somewhat long example, and it might be more involved than necessary if you just want simple style support. But all the elements are in there, the most important ones I mentioned within the last two paragraphs.
    I hope this helps instead of confusing you. And if anybody has a suggestion to improve this code, let me know. ;)



  • I see that basically the best/fastest way of implementing themes support in Qt apps is by using stylesheets. This makes sense since it's highly integrated into the Qt framework. I'm getting back to reading the docs anyway since looks to me like I've missed some points on using stylesheets.

    So thank you very much for your answers guys, this has definitely pointed me in the right direction and I think I'll be using stylesheets for adding themes support in my apps.
    Anyway, if someone else has other suggestions they're always welcome!



  • I thought you wanted to use sylesheets anyway, maybe I misunderstood you. But you can apply them dynamically and you can even change them on the fly, without recompiling anything. Creating a custom style (not sheet) is probably more involved, at least it looks a lot more intimidating than just writing a stylesheet.
    Plus, there are quite some stylesheets available for other applications, so you have a lot of "examples", while I never really dug up a "binary" style in form of some QStyle subclass.



  • Using stylesheets wasn't my first bet, I was actually looking for anything that would customize the look of my application - stylesheets just got in the way and they look easy to make and use.
    QStyle sublcassing is also useful for customizing the look, but only for what it comes to customizing the Widget from the very bottom of it, starting on how and which things are drawn (like images, labels, their position, etc.). For customizing the look only (say, the background image or the text color) I think there is definitely too much work to be done while reimplementing the painting methods. In the QStyle example link that I posted in reply n.2 you can clearly see how much work is needed, while a stylesheet would've been way more simpler for applying the images only.



  • this got me looking at my own code again and have just implemented a ThemeManager

    each window has a
    @AWindow::loadTheme(QString themeName = "original" )
    {
    ThemeManager::loadTheme(themeName);
    setStyleSheet(ThemeManager::getStyleFromName("MAIN_WINDOW_STYLE"));
    ui->pb_Menu->setStyleSheet (ThemeManager::getStyle(ui->pb_Menu));
    // also call any child windows loadTheme() here...
    }@

    thememanager is pretty simple:
    thememanager.h
    @#ifndef THEMEMANAGER_H
    #define THEMEMANAGER_H

    #include <QString>
    #include <QHash>
    #include <QObject>

    class ThemeManager
    {
    public:
    static bool loadTheme(QString themeName = "original");
    static QString getStyle(QWidget* instance);
    static QString getStyleFromName(QString styleName);
    static QString getCurrentTheme() { return currentTheme; }
    private:
    ThemeManager();
    static QHash<QString, QString> themeList;
    static QString currentTheme;
    };
    #endif // THEMEMANAGER_H@

    thememanager.cpp
    @#include "thememanager.h"
    #include <QFile>
    #include <QVariant>
    #include <QDebug>
    #include "mainwindow.h"

    QHash<QString, QString> ThemeManager::themeList;
    QString ThemeManager::currentTheme = "";

    ThemeManager::ThemeManager()
    {
    }
    bool ThemeManager::loadTheme(QString themeName)
    {
    bool retValue = false;
    if( currentTheme.compare(themeName,Qt::CaseInsensitive) == 0)
    return true;
    currentTheme = "<loading>";
    themeList.clear();

    // try to locate the file in /opt/bla/etc/themes/themename.css first
    QString fileName = "/opt/bla/etc/themes/" + themeName + ".css";
    qDebug() << "loading style file from " << fileName;
    if( QFile::exists(fileName) )
    {
        QFile file&#40;fileName&#41;;
        if(!file.open(QIODevice::ReadOnly&#41;&#41;
        {
            qDebug() << "error reading css: "  << file.errorString();
            return false;
        }
        QTextStream in(&file);
        while(!in.atEnd())
        {
            QString line = in.readLine().trimmed();
            if( !line.startsWith("#")) // comments allowed
            {
                // find the first =
                int location = line.indexOf("=");
                if( location != -1 )
                {
                    QString name = line.mid(0,location).trimmed();
                    QString value = line.mid(location+1).trimmed();
    
                    if( value.contains("$"))
                    {
                        // go through the value and find any $VALUES and replace them with previously added items
                        QStringList wordlist = value.split(" ");
                        for(int t=0; t < wordlist.size(); ++t)
                        {
                            QString aword = wordlist[t].trimmed();
                            if( aword.startsWith("$"))
                            {
                                QString searchkey = aword.mid(1);
                                if( themeList.contains(searchkey))
                                {
                                    QVariant a = themeList.value(searchkey);
                                    value.replace(aword, a.toString());
                                }
                            }
                        }
                    }
                    qDebug() << " found " << name << ":" << value;
                    themeList.insert(name, value);
                }
            }
        }
        file.close();
        qDebug() << "loaded css file: " ;
        retValue = true;
    }
    
    currentTheme = themeName;
    return retValue;
    

    }

    QString ThemeManager::getStyle(QWidget* instance)
    {
    QString name = instance->objectName();
    if( instance->parent() != NULL)
    name = instance->parent()->objectName() + "-" + name;

    qDebug() << "first looking for:" << name;
    if( themeList.keys().contains( name));
    {
        qDebug() << "found: " << name;
        QVariant qv = themeList.value(name);
        if( qv.toString().size() > 0)
        {
            qDebug() << "found value: " << qv.toString();
            return qv.toString();
        }
    }
    
    name = instance->objectName();
    // find it by object name
    if( themeList.keys().contains( name));
    {
        qDebug() << "found: " << name;
        QVariant qv = themeList.value(name);
        if( qv.toString().size() > 0)
        {
            qDebug() << "found value: " << qv.toString();
            return qv.toString();
        }
    }
    
    // else find it by generic class
    if( themeList.keys().contains( instance->metaObject()->className()) )
    {
        qDebug() << "found: " << instance->metaObject()->className();
        QVariant qv = themeList.value(instance->metaObject()->className());
        return qv.toString();
    }
    

    // perhaps later also by parentname-generic class name
    return "";
    }

    QString ThemeManager::getStyleFromName(QString styleName)
    {
    if( themeList.contains( styleName));
    {
    QVariant qv = themeList.value(styleName);
    qDebug() << "returning : " << qv;
    return qv.toString();
    }
    return "";
    }@



  • and the ini files looks something like: original.css

    @TEXT_COLOR = #eeeeee
    BACKGROUND_COLOUR = #555555
    TEXT_COLOR = #eeeeee
    TEXT_STYLE = color: $TEXT_COLOR

    LARGE_FONT = font: 27pt Arial
    MEDIUM_FONT = font: 18pt Arial
    SMALL_FONT = font: 14pt Arial

    PB_BORDER_WIDTH = 4px solid
    PB_BORDER_WIDTH_PUSHED = 2px solid
    PB_BORDER_WIDTH_DISABLED = 1px solid

    PB_BORDER_COLOUR = #bbbbbb

    PB_BORDER_RADIUS = border-radius: 20px

    PB_WIDTH = min-width: 80px; max-width: 80px
    PB_HEIGTH = min-height:50px; max-height:50px

    PB_SMALL_SCREEN_WIDTH = min-width: 80px; max-width: 180px
    PB_SMALL_SCREEN_HEIGTH = min-height:40px; max-height: 45px

    PB_BG_GRADIENT = qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgba(100, 100, 100, 255), stop: 1 rgba(32, 32, 32, 255))
    PB_PRESSED_BG_GRADIENT = qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 rgba(100, 100, 100, 255), stop: 1 rgba(32, 32, 32, 255))

    PB_STYLE = QPushButton { $SMALL_FONT ; border: $PB_BORDER_WIDTH $PB_BORDER_COLOUR ; $PB_BORDER_RADIUS ; $TEXT_STYLE ; background-color: $PB_BG_GRADIENT ; $PB_SMALL_SCREEN_WIDTH ; $PB_SMALL_SCREEN_HEIGTH ; } QPushButton:pressed { border: $PB_BORDER_WIDTH_PUSHED #333333; $PB_BORDER_RADIUS ; background-color: $PB_PRESSED_BG_GRADIENT ; $PB_SMALL_SCREEN_WIDTH ; $PB_SMALL_SCREEN_HEIGTH ; } QPushButton:checked { border: $PB_BORDER_WIDTH white; $PB_BORDER_RADIUS ; $PB_SMALL_SCREEN_WIDTH ; $PB_SMALL_SCREEN_HEIGTH ; } QPushButton:disabled { border: $PB_BORDER_WIDTH_DISABLED #bbbbbb; $PB_BORDER_RADIUS ; background-color: $PB_BG_GRADIENT ; color: rgb(100, 100, 100); $PB_SMALL_SCREEN_WIDTH ; $PB_SMALL_SCREEN_HEIGTH ; }

    MAIN_WINDOW_BACKGROUND = background-color: $BACKGROUND_COLOUR
    MAIN_WINDOW_STYLE = $MAIN_WINDOW_BACKGROUND ; color: rgb(238, 238, 238);

    Numberpad_Widget-pb_Menu = $PB_STYLE

    @



  • Hey, that's some impressive work, thank you very much for sharing!
    Just one question: you seem to load .qss files at first, but then you use a INIs. Is there any particular reason for this?



  • yes agreed, the filename extension naming is misleading.. the files contain css but are in ini file format like

    name = value

    where the value is the css

    it was what i could implement the quickest..
    also please note that if you are using variables ( in $NAME ) inside a value, then the NAME has to be defined/specified as a name earlier in the file

    feel free to change at will



  • I see. While I can't find a proper reason for loading QSS from INI stored values, I still think that this code is very useful! If I/somone doesn't like this method it can easily be changed to loading a .QSS file or some other file format that better suits the application (I can think of XML right now).
    So thank you again ;)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.