The default window manager for KDE is Kwin. Kwin has a very flexible theming mechanism, because all themes are ultimately plugins to the window manager. This allows you to change both the look (appearance) and feel (behavior) of the window manager.
This tutorial covers creating a native Kwin theme using C++ code, as opposed to creating a theme for "pixmap engine", such as the IceWM plugin. The material presented here may be of use in creating a new pixmap engine however.
The sample code for this tutorial, along with a copy of this documentation, can be found here: example.tar.gz [36K]. The code snippets shown in this documentation are not documented, but the source code itself is.
This tutorial is assuming that you have some experience with C++ and the Qt toolkit. Specifically, it assumes that you understand C++ inheritance, the Qt signal/slot paradigm, and are familiar with the common Qt classes. If you have never done any Qt programming, please stop right here and take some time out to familiarize yourself with this excellent toolkit. It comes with some of the best documentation for a development library that I have seen.
Note: Currently this tutorial and accompanying example code are based on KDE 3.1.2. The API for Kwin is expected to change in the future. When it does this tutorial will be appropriate updated.
Before you start working on a new theme, you should have some idea as to what the finished theme will look like. All usable window managers will have a title bar containing buttons and a frame which surround the application. There are two classic layouts for window manager decorations, with some minor variations. For lack of standard names, I'll call them the "motif" and "next" layouts.
The "motif" layout | The "next" layout |
---|---|
![]() |
![]() |
The "motif" layout is the most popular. The title bar and window are surrounded by a thick frame. Sometimes the frame only wraps the left, right and bottom of the window, with the title bar being the top part of the frame.
The "next" layout was first seen, to the best of my knowledge, in the NeXT desktop. It is used by the Windowmaker and Blackbox window managers, as well as several KDE themes. There is only a titlebar on top and a "handle" on the bottom.
The example theme will use the motif layout because it is traditional. Since this is an example, I'm going to make everything look quite plain with simple lines delineating the parts of the decoration. Think of the example as a template from which to create your own, more aesthetic, themes.
Drawing out the theme in a paint program like GIMP is very helpful. Sometimes what you think will look good won't, once you actually see it.
Always keep the usability of the theme in mind. No matter how gorgeous the look, no one will use it if it keeps getting in their way. Some simple rules will keep your theme usable by most people. If you must break any of the following rules, please have a good reason to do so.
To start out your project, you'll need a build infrastructure. This will be similar to most KDE applications. Surprisingly, it's an area a lot of people skimp on. The default infrastructure is based on GNU's automake/autoconf framework. There should be very little you need to change from the example code provided.
There should be an admin
directory that
contains the standard KDE admin files. A lot of work has been
done to make your life as a KDE developer easy, and most of
it is in this directory. I have not included this directory
in the example package, but you can obtain this from any KDE
source package. The best place is to copy it from a current
kdesdk
package.
Please include a README
and
INSTALL
file. Describe what this theme is, and
how to install it. And by all means, please state what
license your code is under! There's nothing worse than
wanting to borrow a piece of code, and not knowing if you're
legally entitled to. The preferred location for this
information is the COPYING
file. The GPL is the
most popular for KDE programs, but many themes are under the
MIT license, since that's what Kwin is under.
If you're using the example package as a template, you
will need to modify the configure.in.in
,
client/Makefile.am
,
client/config/Makefile.am
, and
example.desktop
files. Simply replace all
occurances of "example" with the name of your theme. If you
add additional source code files, you'll need to change these
files as appropriate. Please see the
KDE Developer FAQ for more information on the standard
KDE build infrastructure.
If there are any user configurable aspects to your theme, you should create a configuration dialog. The configuration dialog is actually a plugin to Control Center. Common configuration options for Kwin themes include title alignment, title bar height, drawing frames using the titlebar colors, and displaying an additional handle. For the example, we will be using only one option to specify the title alignment.
I have chosen to use Qt Designer to handle the layout of the dialog. This makes it very easy to arrange the dialog. Add three QRadio buttons in a QButtonGroup box to let the user choose the alignment of the title text. Add some "What's This" help text. Designer will make all of the widgets public so that you can easily access them from your configuration plugin.
The exampleconfig.h
header file is quite
short, and has only two member variables, one for the
configuration, and one for the actual dialog:
... private: KConfig *config_; ConfigDialog *dialog_;
The exampleconfig.cc
is almost as simple.
Since most of the GUI work is being done by Qt Designer in
our ui file, all we need to do is worry about is the
configuration. The constructor creates a new configuration
object and dialog, and connects with the dialog.
ExampleConfig::ExampleConfig(KConfig* config, QWidget* parent) : QObject(parent), config_(0), dialog_(0) { config_ = new KConfig("kwinexamplerc"); KGlobal::locale()->insertCatalogue("kwin_example_config"); dialog_ = new ConfigDialog(parent); dialog_->show(); load(config); connect(dialog_->titlealign, SIGNAL(clicked(int)), this, SLOT(selectionChanged(int))); }
Our work consists of loading and saving the configuration, and setting some sensible defaults. We simply set the dialog widgets to the existing configuration, and write them out again when the change.
void ExampleConfig::load(KConfig*) { config_->setGroup("General"); QString value = config_->readEntry("TitleAlignment", "AlignHCenter"); QRadioButton *button = (QRadioButton*)dialog_->titlealign->child(value); if (button) button->setChecked(true); } void ExampleConfig::save(KConfig*) { config_->setGroup("General"); QRadioButton *button = (QRadioButton*)dialog_->titlealign->selected(); if (button) config_->writeEntry("TitleAlignment", QString(button->name())); config_->sync(); }
Each window is known as a "client", and is represented by the Client class. If you have five windows up on the screen, you will have five instances of Client. Managing the theme for multiple clients is the job of the handler. Not every theme needs a handler class, but it simplifies many things.
We'll use the ExampleHandler to take care of storing the configuration data. It could also be used to create and store the pixmaps used by the clients. Think of the handler as a global resource for your clients.
void ExampleHandler::reset() { readConfig(); initialized_ = true; Workspace::self()->slotResetAllClientsDelayed(); }
The constructor merely calls the reset()
method, which reads in the configuration and resets all
clients. We need to reset all the clients whenever the
configuration has changed. Kwin does this for us via the
slotResetAllClientsDelayed()
call.
void ExampleHandler::readConfig() { KConfig config("kwinexamplerc"); config.setGroup("General"); QString value = config.readEntry("TitleAlignment", "AlignHCenter"); if (value == "AlignLeft") titlealign_ = Qt::AlignLeft; else if (value == "AlignHCenter") titlealign_ = Qt::AlignHCenter; else if (value == "AlignRight") titlealign_ = Qt::AlignRight; }
Reading in the configuration is very similar to how we
read it in the configuration dialog. By assigning the
titlealign_
member to the title alignment,
clients can determine their title alignment without having to
load the configuration. They will do this with
ExampleHandler's titleAlign()
method.
The buttons on the titlebar are derived from the KWinButton class, which are ordinary QButtons. But we still need to determine their look and behavior.
ExampleButton::ExampleButton(Client *parent, const char *name, const QString& tip, ButtonType type, const unsigned char *bitmap) : KWinButton(parent, name, tip), client_(parent), type_(type), deco_(0), lastmouse_(0) { setBackgroundMode(NoBackground); setFixedSize(BUTTONSIZE, BUTTONSIZE); if (bitmap) setBitmap(bitmap); }
The constructor sets the size of the button then sets the bitmap decoration. The decoration is what visually distinguishes the buttons from each other. The close button will have an "x" decoration, while the minimize button will have a "v" decoration.
void ExampleButton::setBitmap(const unsigned char *bitmap) { if (!bitmap) return; if (deco_) delete deco_; deco_ = new QBitmap(DECOSIZE, DECOSIZE, bitmap, true); deco_->setMask(*deco_); repaint(false); }
A QBitmap has only two colors, foreground and background. If we wanted more colors for our decorations we could have used QPixmap instead. We repaint the button every time the bitmap is changed.
For the menu button we do use a QPixmap. In this case we will use the application icon. We will access this pixmap from the Client when we draw the button.
void ExampleButton::enterEvent(QEvent *e) { KWinButton::enterEvent(e); } void ExampleButton::leaveEvent(QEvent *e) { KWinButton::leaveEvent(e); }
We don't do anything with the enter and leave events except pass them on to the base class. If we wanted to implement "mouse over" highlighting of the buttons, however, this is where we would start.
void ExampleButton::mousePressEvent(QMouseEvent* e) { lastmouse_ = e->button(); QMouseEvent me(e->type(), e->pos(), e->globalPos(), LeftButton, e->state()); KWinButton::mousePressEvent(&me); } void ExampleButton::mouseReleaseEvent(QMouseEvent* e) { lastmouse_ = e->button(); QMouseEvent me(e->type(), e->pos(), e->globalPos(), LeftButton, e->state()); KWinButton::mouseReleaseEvent(&me); }
These two events tell us about mouse clicks. These are just the events, and not the signals, so we don't perform any actual windowing behaviors, such as minimize or close. But we do want to remember which mouse button did the clicking. That way if the maximize button was pressed, we will know whether to maximize horizontally, vertically or full.
Notice that we pass on the event after setting the mouse button to "LeftButton". This is a workaround for Kwin bug. Try it without this and see what happens. In future fixed versions of Kwin we will simply pass on the event unchanged.
void ExampleButton::drawButton(QPainter *painter) { if (!ExampleHandler::initialized()) return; QColorGroup group; int dx, dy; ...
The last method to ExampleButton is to draw the button. We return if the handler has not been initialized. This should never happen, but it's better to be safe than to have several hundred bug reports concerning mysterious crashes.
... group = options->colorGroup(Options::ButtonBg, client_->isActive()); painter->fillRect(rect(), group.button()); painter->setPen(group.dark()); painter->drawRect(rect()); ...
There are an infinite number of ways to draw the button. For the purposes of this example, we merely draw a blank button with a dark border around it.
Notice the call to options
object. Kwin keeps
track of several configuration items which we can access
through the global Options
class. One of these
items is a QColorGroup generated from the user defined button
color.
... if (type_ == ButtonMenu) { // we paint the mini icon (which is 16 pixels high) dx = (width() - 16) / 2; dy = (height() - 16) / 2; painter->drawPixmap(dx, dy, client_->miniIcon()); } else { // otherwise we paint the deco dx = (width() - DECOSIZE) / 2; dy = (height() - DECOSIZE) / 2; if (isDown()) { dx++; dy++; } painter->setPen(group.dark()); if (deco_) painter->drawPixmap(dx, dy, *deco_); } }
Finally we paint the button decoration. We do some minor calculations to center the decoration in the button. If this is the menu button, we draw the application icon. If it's any other button, we draw the bitmap decoration.
Minimize Button | Menu Button |
---|---|
![]() |
![]() |
A common effect for buttons is to have make it look "pressed" when the mouse clicks on it, and it's in the "down" state. We do this by shifting the position of the decoration slightly.
The ExampleClient class is the heart of the theme. Is provides most of the theming, and contains the application window and the title bar buttons.
ExampleClient::ExampleClient(Workspace *ws, WId w, QWidget *parent, const char *name) : Client(ws, w, parent, name, WResizeNoErase | WRepaintNoErase), titlebar_(0) { setBackgroundMode(NoBackground); ...
In the constructor we pass a couple of WFlags to the base
class. This helps eliminate some flicker when resizing and
repainting windows. We also set a NoBackground
mode for the same reason.
... QGridLayout *mainlayout = new QGridLayout(this, 4, 3); // 4x3 grid QHBoxLayout *titlelayout = new QHBoxLayout(); titlebar_ = new QSpacerItem(1, TITLESIZE, QSizePolicy::Expanding, QSizePolicy::Fixed); mainlayout->setResizeMode(QLayout::FreeResize); mainlayout->addRowSpacing(0, FRAMESIZE); mainlayout->addRowSpacing(3, FRAMESIZE*2); mainlayout->addColSpacing(0, FRAMESIZE); mainlayout->addColSpacing(2, FRAMESIZE); mainlayout->addLayout(titlelayout, 1, 1); mainlayout->addWidget(windowWrapper(), 2, 1); ...
Qt's layout classes are very flexible and powerful, so we use them to layout our theme. We create a four row by three column grid. See the "motif" diagram above for a visual explanation. The central window is the only widget that will go into this grid.
The only objects we actually add to the main layout is the
titlelayout
and window. The outer rows and
columns around them are empty spacing. We will later use this
spacing to draw the window frame.
... mainlayout->setRowStretch(2, 10); mainlayout->setColStretch(1, 10); ...
It is important to ensure that only the central window changes size when the window is resized. We do this by setting the stretch for the central row and column. Try removing these two lines to see what happens without it. It's not pretty.
... for (int n=0; n<ButtonTypeCount; n++) button[n] = 0; addButtons(titlelayout, options->titleButtonsLeft()); titlelayout->addItem(titlebar_); addButtons(titlelayout, options->titleButtonsRight()); }
To finish up the contructor we layout the titlebar. The
global options
object will tell us the button
layout that the user has chosen. Between the left and the
right buttons is the titlebar_
spacer we
created. This spacer will stretch horizontally as needed, but
keep a fixed vertical height.
I have created an addButton()
method to ease
the creation and layout of the title bar buttons. Most themes
that honor custom button layouts use a similar method. The
function is lengthy, but the concept is simple.
void ExampleClient::addButtons(QBoxLayout *layout, const QString& s) { if (s.length() > 0) { for (unsigned n=0; n < s.length(); n++) { switch (s[n]) { case 'M': // Menu button if (!button[ButtonMenu]) { button[ButtonMenu] = new ExampleButton(this, "menu", i18n("Menu"), ButtonMenu, 0); connect(button[ButtonMenu], SIGNAL(pressed()), this, SLOT(menuButtonPressed())); layout->addWidget(button[ButtonMenu]); } break; ...
A string is passed in representing the button positions,
and the layout the buttons are to be added to. We go through
the string one character at a time. For each character we
construct a button. In the first case shown above, the
character 'M' constructs a menu button, connects the pressed
signal to the menuButtonPressed()
method, and
adds it to the layout.
Notice that we are keeping our buttons points in an array. This makes it easy to access all buttons at once, as we did when we initialized all the pointers to null in the constructor. An enum is used to conveniently access them
Only the menu button is shown here. The other buttons are similar.
... case '_': // Spacer item layout->addSpacing(FRAMESIZE); } } } }
A spacing in the button layout is a special case. In the example theme we merely add a small bit of spacing to the layout.
The finished title bar layout will look like this:
void ExampleClient::paintEvent(QPaintEvent*) { if (!ExampleHandler::initialized()) return; QPainter painter(this); QColorGroup group; ...
Painting the parts of the window is simply a matter of determining the coordinates of the various parts, then using the standard QPainter methods to draw. The only parts we need to worry about are the titlebar and the outside frame. The buttons and the window will draw themselves.
... QRect title(titlebar_->geometry()); group = options->colorGroup(Options::TitleBar, isActive()); painter.fillRect(title, group.background()); painter.setPen(group.dark()); painter.drawRect(title); ...
To draw the titlebar we get its coordinates, set the color group, fill it with the background color, then outline it with a dark color.
For the example this is good enough. Other possibilities are to draw a bevel effect around the title, using a gradient or custom pixmap, or even fancier techniques.
... painter.setFont(options->font(isActive(), false)); painter.setPen(options->color(Options::Font, isActive())); painter.drawText(title.x() + FRAMESIZE, title.y(), title.width() - FRAMESIZE * 2, title.height(), ExampleHandler::titleAlign() | AlignVCenter, caption()); ...
The options
object also gives us the font and
font color. The false
parameter when retrieving
the font indicates that we want the normal font. If we had
set it to true
we would have gotten a smaller
font suitable for use in tool window titlebars.
A small space is left on the left and right sides, so that the title text doesn't run up against the buttons.
... group = options->colorGroup(Options::Frame, isActive()); QRect frame(0, 0, width(), FRAMESIZE); painter.fillRect(frame, group.background()); frame.setRect(0, 0, FRAMESIZE, height()); painter.fillRect(frame, group.background()); frame.setRect(0, height() - FRAMESIZE*2, width(), FRAMESIZE*2); painter.fillRect(frame, group.background()); frame.setRect(width()-FRAMESIZE, 0, FRAMESIZE, height()); painter.fillRect(frame, group.background()); painter.setPen(group.dark()); frame = rect(); painter.drawRect(frame); frame.setRect(frame.x() + FRAMESIZE-1, frame.y() + FRAMESIZE-1, frame.width() - FRAMESIZE*2 +2, frame.height() - FRAMESIZE*3 +2); painter.drawRect(frame); }
Drawing the outer frame involves a more complex geometry. We simplify this by separating it into four parts. The corners of the frame will each be drawn twice, due to the overlap, but for simple fills, this does not result in any performance loss. Notice that the frame geometry matches the spacing we put in the main layout.
We've finished all of our drawing code. But we're still far from done with the client class. There are events we have to take care of. Some of these will be X11 or Qt events. Others will be Kwin state changes or user actions.
void ExampleClient::mouseDoubleClickEvent(QMouseEvent *e) { if (titlebar_->geometry().contains(e->pos())) workspace()->performWindowOperation(this, options->operationTitlebarDblClick()); }
When the user double clicks in our window, we need to
decide what to do. The only place where a double click does
anything is in the title bar, so we first check to see if
that's where the event happened. If so, we tell Kwin to
perform the operationTitlebarDblClick()
action.
Usually this shades or unshades the window, but the user can
redefine this behavior.
void ExampleClient::activeChange(bool) { for (int n=0; n<ButtonTypeCount; n++) if (button[n]) button[n]->repaint(false); repaint(false); }
A window is only active when it has the focus. The typical Kwin behavior is to use a different color scheme for active and inactive windows. When the focus state changes, we need to redraw the window because it will have a different color scheme. This is a simple matter of redrawing all the buttons, and then the client.
void ExampleClient::maximizeChange(bool m) { if (button[ButtonMax]) { button[ButtonMax]->setBitmap(m ? minmax_bits : max_bits); button[ButtonMax]->setTipText(m ? i18n("Restore") : i18n("Maximize")); } }
When the window has changed its maximize state, we need to change the button decoration for the maximize button. We also need to change the "tooltip" text
void ExampleClient::maxButtonPressed() { if (button[ButtonMax]) { switch (button[ButtonMax]->lastMousePress()) { case MidButton: maximize(MaximizeVertical); break; case RightButton: maximize(MaximizeHorizontal); break; default: maximize(); } } }
The expected behavior for Kwin maximize buttons is to maximize horizontally if the right button is pressed, maximize vertically if the middle button is pressed, and maximize fully if the left button was pressed. We know which mouse button was pressed because we had the button save that information for us earlier.
void ExampleClient::menuButtonPressed() { if (button[ButtonMenu]) { QPoint pt(button[ButtonMenu]->rect().bottomLeft().x(), button[ButtonMenu]->rect().bottomLeft().y()); workspace()->showWindowMenu(button[ButtonMenu]->mapToGlobal(pt), this); button[ButtonMenu]->setDown(false); } }
Clicking in the menu button brings up the window menu. We
want this menu positioned just below our button, so we pass
this point to Kwin's showWindowMenu()
method.
For some themes, a double click in the menu button will close the window. There is a small controversy over whether this is desirable behavior. I have decided not to implement "double-click to close" functionality for the example.
I haven't shown all of the example source code here. But
it's all included in the example.tar.gz package. Besides
reading though the example, also look at the
client.h
and options.h
header files
located in $KDEDIR/include/kwin
. The former is
your theme's base class, and the latter provides you with
access to a lot of configuration information. Also look at
other Kwin themes.
The finished product isn't pretty, but it's fully functional. Think of it as your starting point for your own Kwin theme.
Here's a list of simple exercises to get you some hands on knowledge of Kwin themes