Development/Tutorials/KDE2/Extending the KDE Panel: Difference between revisions
Neverendingo (talk | contribs) m (Text replace - "</code>" to "</syntaxhighlight>") |
|||
(21 intermediate revisions by 5 users not shown) | |||
Line 1: | Line 1: | ||
[[Category:KDE2]] | |||
== Introduction == | |||
[[Category: | |||
The KDE panel (below referred to as "kicker") has been rewritten from scratch for KDE 2.0 and one of the main goals of the rewrite was to increase extensibility. For this reason with KDE 2.0 an API for panel applets has been introduced followed by an API for panel extension introduced with the KDE 2.1 release. Panel applets and extensions are simply referred to as "plugins" in the following paragraphs. | The KDE panel (below referred to as "kicker") has been rewritten from scratch for KDE 2.0 and one of the main goals of the rewrite was to increase extensibility. For this reason with KDE 2.0 an API for panel applets has been introduced followed by an API for panel extension introduced with the KDE 2.1 release. Panel applets and extensions are simply referred to as "plugins" in the following paragraphs. | ||
Panel applets are small applications living inside the panel. Available applets range from desktop pagers and task bars to little games or other toys. In contrast to applets the panel extension API is an interface for extensions living outside of the panel, in the window manager dock area (as defined in the [http://www.freedesktop.org/standards/wm-spec.html freedesktop.org window manager specifications] and are managed by the panel. Examples for panel extensions are the external task bar and the dock application bar which adds support for [http://www.windowmaker.org Window Maker] applets and other applications using the standard X11 docking mechanism to kicker. | Panel applets are small applications living inside the panel. Available applets range from desktop pagers and task bars to little games or other toys. In contrast to applets the panel extension API is an interface for extensions living outside of the panel, in the window manager dock area (as defined in the [http://www.freedesktop.org/standards/wm-spec.html freedesktop.org window manager specifications] and are managed by the panel. Examples for panel extensions are the external task bar and the dock application bar which adds support for [http://www.windowmaker.org Window Maker] applets and other applications using the standard X11 docking mechanism to kicker. | ||
Starting with a short technology overview this tutorial discusses the implementation of a simple panel applet. Both the applet and extension APIs are simple. This qualifies writing a panel applet as a suitable task for an introduction to KDE programming. This tutorial presumes that the reader has some basic C++ and Qt knowledge. You can | Starting with a short technology overview this tutorial discusses the implementation of a simple panel applet. Both the applet and extension APIs are simple. This qualifies writing a panel applet as a suitable task for an introduction to KDE programming. This tutorial presumes that the reader has some basic C++ and Qt knowledge. You can also start with [http://doc.trolltech.com/2.3/tutorial.html a basic Qt2 tutorial]. | ||
== Overview == | == Overview == | ||
Panel plugins are implemented as DSO s (Dynamic Shared Objects). The panel locates available applets by searching for applet description files in (ALL_KDEDIRS)/share/apps/kicker/applets Every plugin must install a description file to be recognized by the panel. Available panel extensions are located by searching (A_KDEDIR)/share/apps/kicker/extensions for similar description files. | Panel plugins are implemented as DSO s (Dynamic Shared Objects). The panel locates available applets by searching for applet description files in {{path|(ALL_KDEDIRS)/share/apps/kicker/applets}}. Every plugin must install a description file to be recognized by the panel. Available panel extensions are located by searching {{path|(A_KDEDIR)/share/apps/kicker/extensions}} for similar description files. | ||
Implementing a panel applet is as easy as inheriting from the base class KPanelApplet (declared in kdelibs/kdeui/kpanelapplet.h), providing a factory function and installing the description file mentioned above. For extensions the respective base class is KPanelExtension , declared in kdelibs/kdeui/kpanelextension.h. | Implementing a panel applet is as easy as inheriting from the base class KPanelApplet (declared in {{path|kdelibs/kdeui/kpanelapplet.h}}), providing a factory function and installing the description file mentioned above. For extensions the respective base class is KPanelExtension, declared in {{path|kdelibs/kdeui/kpanelextension.h}}. | ||
While plugins are implemented as shared libraries and thus loaded into the panel's address space, for security and stability reasons kicker also implements loading of plugins via external proxy processes called appletproxy and extensionproxy. The loading behavior is user configurable via kcontrol, with the obvious benefit that users may configure their panel to load untrusted third party plugins via the proxies to avoid buggy plugins taking the whole panel down with them. | While plugins are implemented as shared libraries and thus loaded into the panel's address space, for security and stability reasons kicker also implements loading of plugins via external proxy processes called appletproxy and extensionproxy. The loading behavior is user configurable via kcontrol, with the obvious benefit that users may configure their panel to load untrusted third party plugins via the proxies to avoid buggy plugins taking the whole panel down with them. | ||
While as an plugin author you don't have to be concerned about these implementation details it is good to know what happens behind the scenes. Therefore I might mention here that the panel communicates with the proxies via the DCOP protocol (see kdelibs/dcop) and makes use of QXEmbed (see kdelibs/kdeui/qxembed.h) to embed external (loaded via one of the two proxies) plugins into the panel. | While as an plugin author you don't have to be concerned about these implementation details it is good to know what happens behind the scenes. Therefore I might mention here that the panel communicates with the proxies via the DCOP protocol (see {{path|kdelibs/dcop}}) and makes use of QXEmbed (see {{path|kdelibs/kdeui/qxembed.h}}) to embed external (loaded via one of the two proxies) plugins into the panel. | ||
== A hello world panel applet == | == A hello world panel applet == | ||
We start with a hello world panel applet that can be implemented in only a few lines of code and is a good base upon which to build a more complex applet. As already mentioned in the overview section all that needs to be done is inheriting from KPanelApplet , providing a factory function and a .desktop description file. | We start with a hello world panel applet that can be implemented in only a few lines of code and is a good base upon which to build a more complex applet. As already mentioned in the overview section all that needs to be done is inheriting from KPanelApplet, providing a factory function and a {{path|.desktop}} description file. | ||
Our little project consists of four files: helloworldapplet.h containing the class declaration, helloworldapplet.cpp containing the class implementation and the factory function, helloworldapplet.desktop the description file and Makefile.am as input for the automake/autoconf KDE build system. | Our little project consists of four files: {{path|helloworldapplet.h}} containing the class declaration, {{path|helloworldapplet.cpp}} containing the class implementation and the factory function, {{path|helloworldapplet.desktop}} the description file and {{path|Makefile.am}} as input for the automake/autoconf KDE build system. | ||
== HelloWorldApplet class declaration == | === HelloWorldApplet class declaration === | ||
< | <syntaxhighlight lang="cpp-qt" line> | ||
class HelloWorldApplet : public KPanelApplet | class HelloWorldApplet : public KPanelApplet | ||
{ | { | ||
Line 42: | Line 39: | ||
}; | }; | ||
</ | </syntaxhighlight> | ||
Having a look at kpanelapplet.h see that all we do is re-implement the constant virtual functions int widthForHeight(int) and heightForWidth(int) Playing around with kicker you may notice that you can arrange the panel either horizontally or vertically at the desktop borders. You will also notice that there is a fixed set of panel sizes (representing panel height for horizontal panels and width for vertical panels) to choose from. | Having a look at {{path|kpanelapplet.h}} see that all we do is re-implement the constant virtual functions int widthForHeight(int) and heightForWidth(int) Playing around with kicker you may notice that you can arrange the panel either horizontally or vertically at the desktop borders. You will also notice that there is a fixed set of panel sizes (representing panel height for horizontal panels and width for vertical panels) to choose from. | ||
The concept of panel applets is that one size component is dictated by the panel while the other one is free for the applet to choose. On horizontal panels the applet's height is fixed according to the panel height while the applet is free to choose its width. Respectively on vertical panels the applet's height is fixed while it is free to choose its height. | The concept of panel applets is that one size component is dictated by the panel while the other one is free for the applet to choose. On horizontal panels the applet's height is fixed according to the panel height while the applet is free to choose its width. Respectively on vertical panels the applet's height is fixed while it is free to choose its height. | ||
Line 50: | Line 47: | ||
Every panel applet should be prepared to be used both on horizontal and vertical panels. The panel uses the two functions we are re-implementing for the HelloWorldApplet to query the applet's preferred size. A horizontal panel will call widthForHeight() while a vertical panel will call heightForWidth() to query the free size component of the applet. Applets are guaranteed to be resized according to the size they request. | Every panel applet should be prepared to be used both on horizontal and vertical panels. The panel uses the two functions we are re-implementing for the HelloWorldApplet to query the applet's preferred size. A horizontal panel will call widthForHeight() while a vertical panel will call heightForWidth() to query the free size component of the applet. Applets are guaranteed to be resized according to the size they request. | ||
== HelloWorldApplet class implementation == | === HelloWorldApplet class implementation === | ||
< | <syntaxhighlight lang="cpp-qt" line> | ||
HelloWorldApplet::HelloWorldApplet( const QString& configFile, | HelloWorldApplet::HelloWorldApplet( const QString& configFile, | ||
Type type, int actions, | Type type, int actions, | ||
Line 73: | Line 70: | ||
return w; // we want to be quadratic | return w; // we want to be quadratic | ||
} | } | ||
</ | </syntaxhighlight> | ||
Our constructor simply passes its default arguments to the KPanelApplet constructor, sets the frame style to StyledPanel Sunken (see qframe.h) and sets the background color of the applet to blue so we recognize it later running in the panel. Both widthForHeight() and heightForHeight() are implemented to choose a quadratic geometry for the applet. Thus we expect our applet to show up as a sunken blue square on the panel when run. | Our constructor simply passes its default arguments to the KPanelApplet constructor, sets the frame style to StyledPanel Sunken (see {{path|qframe.h}}) and sets the background color of the applet to blue so we recognize it later running in the panel. Both widthForHeight() and heightForHeight() are implemented to choose a quadratic geometry for the applet. Thus we expect our applet to show up as a sunken blue square on the panel when run. | ||
== Screenshot of 'Hello, World!' applet == | === Screenshot of 'Hello, World!' applet === | ||
[[Image:helloworld_applet.png]] | [[Image:helloworld_applet.png]] | ||
== The factory function == | === The factory function === | ||
< | <syntaxhighlight lang="cpp-qt" line> | ||
extern "C" | extern "C" | ||
{ | { | ||
Line 91: | Line 88: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
The factory function is mostly copy&paste work where you would replace "helloworldapplet" with "myapplet" to adjust it to your custom applet. {{note|Don't change the factory function's signature or the panel will fail to load your applet. After adding the factory function, we are done with the code part and only the description file and the Makefile.am remain to be written.}} | The factory function is mostly copy&paste work where you would replace "helloworldapplet" with "myapplet" to adjust it to your custom applet. {{note|Don't change the factory function's signature or the panel will fail to load your applet. After adding the factory function, we are done with the code part and only the description file and the Makefile.am remain to be written.}} | ||
== The description file: helloworldapplet.desktop == | === The description file: helloworldapplet.desktop === | ||
< | <syntaxhighlight lang="ini"> | ||
[Desktop Entry] | [Desktop Entry] | ||
Name = Hello World | Name = Hello World | ||
Line 102: | Line 99: | ||
X-KDE-Library = libhelloworldapplet | X-KDE-Library = libhelloworldapplet | ||
X-KDE-UniqueApplet = true | X-KDE-UniqueApplet = true | ||
</ | </syntaxhighlight> | ||
Besides standard .desktop file keys like "Name", "Comment" and "Icon" there are two panel applet specific keys: | Besides standard {{path|.desktop}} file keys like "Name", "Comment" and "Icon" there are two panel applet specific keys: | ||
'''X-KDE-Library'''<br> | '''X-KDE-Library'''<br> | ||
Line 110: | Line 107: | ||
Example: X-KDE-Library=libexampleapplet | Example: X-KDE-Library=libexampleapplet | ||
'''X-KDE-UniqueApplet'''<br> | |||
Similar to KApplication and KUniqueApplication there are two types of panel applets. Use unique applets when it makes no sense to run more than one instance of an applet in the panel. A good example for unique applets is the taskbar applet. Use normal applets when you need instance specific configuration. An example is the koolclock applet where you might want to run two instances in your panel, one configured as analog clock, the other one as digital clock. X-KDE-UniqueApplet is a boolean key which defaults to "false". | Similar to KApplication and KUniqueApplication there are two types of panel applets. Use unique applets when it makes no sense to run more than one instance of an applet in the panel. A good example for unique applets is the taskbar applet. Use normal applets when you need instance specific configuration. An example is the koolclock applet where you might want to run two instances in your panel, one configured as analog clock, the other one as digital clock. X-KDE-UniqueApplet is a boolean key which defaults to "false". | ||
Line 122: | Line 120: | ||
'''LDFLAGS'''<br> | '''LDFLAGS'''<br> | ||
-module -no-undefined | -module -no-undefined | ||
=== The automake input file: Makefile.am === | |||
<syntaxhighlight lang="text"> | |||
INCLUDES = $(all_includes) | |||
lib_LTLIBRARIES = libhelloworldapplet.la | |||
libhelloworldapplet_la_SOURCES = helloworldapplet.cpp | |||
METASOURCES = AUTO | |||
noinst_HEADERS = helloworldapplet.h | |||
lnkdir = $(kde_datadir)/kicker/applets | |||
lnk_DATA = helloworldapplet.desktop | |||
EXTRA_DIST = $(lnk_DATA) | |||
libhelloworldapplet_la_LDFLAGS = $(all_libraries) -version-info 1:0:0 -module \ | |||
-no-undefined | |||
libhelloworldapplet_la_LIBADD = $(LIB_KDEUI) | |||
messages: | |||
$(XGETTEXT) *.cpp *.h -o $(podir)/helloworldapplet.pot | |||
</syntaxhighlight> | |||
Explaining the details of this particular automake input file and the KDE build system in general is not the goal of this tutorial. So to adjust it to your applet project simply replace all occurrences of "helloworldapplet" with "myapplet". | |||
=== Building and installing === | |||
You can download a complete helloworldapplet.tar.gz tarball here: [http://developer.kde.org/documentation/tutorials/dot/khelloworldapplet.tar.gz khelloworldapplet.tar.gz] (308 kB), which in addition to the four files mentioned above contains the automake/autoconf magic of the KDE build system. It's convenient to base your own panel applets on as the build system magic is already in place. Untar it, change into the untarred directory and build and install it with: | |||
<syntaxhighlight lang="text"> | |||
./configure -prefix=<your-kde-dir> | |||
make | |||
su -c 'make install' | |||
</syntaxhighlight> | |||
=== Hello world! === | |||
Now that your applet is installed and the description file in place, a "Hello World" entry will show up in the Add-Applet sub menu of the panel menu. Select the "Hello World" entry and you will see our blue sunken quadratic panel applet show up in the panel. You will also notice a small grey handle on the left side of the applet. Use the right mouse button context menu of the applet handle to move or remove the applet. You can also move applets by dragging the handle. | |||
== Fifteen pieces panel applet == | |||
Searching for a panel applet suitable for this tutorial I stumbled over a screenshot of an applet implementation of the old fifteen pieces game for another desktop environment. While it's more a toy than of any real use it is simple and fast to implement and thus perfect for a tutorial. I'm sure you know the fifteen pieces game with the goal to put fifteen sliding pieces into numerical oder. The idea of the game is that on a 4x4 cell quadratic game board fifteen numbered (1 to 15) quadratic pieces must be put into numerical order while pieces can only be moved horizontally or vertically and with only one free cell to perform move operations on. | |||
We are going to use a customized {{qt2|QTableView}} widget for the game board. Similar to the hello world applet our little project will consist of three files, {{path|fiftenapplet.cpp}}, {{path|fifteenapplet.h}}, {{path|fifteenapplet.desktop}} and the {{path|Makefile.am}} file. | |||
{{path|fifteenapplet.h}} contains the declaration of FifteenApplet, the class inheriting from KPanelApplet : | |||
<syntaxhighlight lang="cpp-qt" line> | |||
class FifteenApplet : public KPanelApplet | |||
{ | |||
Q_OBJECT | |||
public: | |||
FifteenApplet(const QString& configFile, Type t = Stretch, | |||
int actions = 0, | |||
QWidget *parent = 0, const char *name = 0); | |||
int widthForHeight(int height) const; | |||
int heightForWidth(int width) const; | |||
void about(); | |||
private: | |||
PiecesTable *_table; | |||
KAboutData *_aboutData; | |||
}; | |||
</syntaxhighlight> | |||
While it is very similar to the declaration of HelloWorldApplet, we have added a reimplementation of void about() (as defined in {{path|kpanelapplet.h}}) and private pointers for our game board class and a KAboutData object (see {{path|kdelibs/kdecore/kaboutdata.h}}) used to build an about dialog from. | |||
Searching for void about() in the KPanelApplet class declaration you will find two similar protected functions void help() and void preferences() In the right mouse button context menu of some applets you find in addition to the already mentioned "Move" and "Remove" entries, entries called "About", "Help" and "Preferences". The three protected functions are action handlers called when a user selects "About", "Help" or "Preferences" from the applets context menu. You have to reimplement them in your applet class to handle them. Not every applet implements for example the "Preferences" action because it might not have any preferences to configure. To avoid unused menu actions in the applet's context menu, kicker will only display those that you configure it to do by passing the according actions you reimplement or'ed together as the third parameter of the applet's class constructor. | |||
The second class declared in {{path|fifteenapplet.h}} is our game board inheriting from QTableView : | |||
<syntaxhighlight lang="cpp-qt" line> | |||
class PiecesTable : public QTableView | |||
{ | |||
Q_OBJECT | |||
public: | |||
PiecesTable(QWidget* parent = 0, const char* name = 0); | |||
protected: | |||
void resizeEvent(QResizeEvent*); | |||
void mousePressEvent(QMouseEvent*); | |||
void mouseMoveEvent(QMouseEvent*); | |||
void paintCell(QPainter *, int row, int col); | |||
void initMap(); | |||
void initColors(); | |||
void randomizeMap(); | |||
void checkwin(); | |||
private: | |||
QArray<int> _map; | |||
QArray<QColor> _colors; | |||
QPopupMenu *_menu; | |||
int _activeRow, _activeCol; | |||
bool _randomized; | |||
enum MenuOp { mRandomize = 1, mReset = 2 }; | |||
}; | |||
</syntaxhighlight> | |||
I'm going to explain the details of this class below by means of the class implementation. | |||
The factory function is very similar to the one of the helloworld applet with all occurrences of "helloworldapplet" replaced with "fifteenapplet". The second difference you might notice is that we pass KPanelApplet::About as the third parameter of the FifteenApplet constructor to make sure the "About" context menu entry will be there: | |||
<syntaxhighlight lang="cpp-qt" line> | |||
extern "C" | |||
{ | |||
KPanelApplet* init(QWidget *parent, const QString& configFile) | |||
{ | |||
KGlobal::locale()->insertCatalogue("kfifteenapplet"); | |||
return new FifteenApplet(configFile, KPanelApplet::Normal, | |||
KPanelApplet::About, parent, "kfifteenapplet"); | |||
} | |||
} | |||
</syntaxhighlight> | |||
The implementation of the FifteenApplet class is very short as the game board class will handle all the drawing and game logic. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
FifteenApplet::FifteenApplet(const QString& configFile, Type type, int actions, | |||
QWidget *parent, const char *name) | |||
: KPanelApplet(configFile, type, actions, parent, name), _aboutData(0) | |||
{ | |||
// setup table | |||
_table = new PiecesTable(this); | |||
// setup layout | |||
QHBoxLayout *_layout = new QHBoxLayout(this); | |||
_layout->add(_table); | |||
srand(time(0)); | |||
} | |||
</syntaxhighlight> | |||
The constructor creates an instance of our game board class and places it into a simple layout object to resize it to the full applet size every time the applet itself is resized. srandom is used to initialize the random number generator with the current time in seconds (see the man pages of srandom and random) as seed. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
int FifteenApplet::widthForHeight(int h) const | |||
{ | |||
return h; // we want to be quadratic | |||
} | |||
int FifteenApplet::heightForWidth(int w) const | |||
{ | |||
return w; // we want to be quadratic | |||
} | |||
</syntaxhighlight> | |||
Similar to the hello world applet the fifteen pieces applet will have a quadratic shape. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void FifteenApplet::about() | |||
{ | |||
if(!_aboutData) { | |||
_aboutData = new KAboutData("kfifteenapplet", I18N_NOOP("KFifteenApplet"), | |||
"1.0", I18N_NOOP("Fifteen pieces applet.\n\n" | |||
"The goal is to put the sliding pieces into numerical order.\n" | |||
"Select \"Randomize Pieces\" from the RMB menu to start a game."), | |||
KAboutData::License_BSD, "(c) 2001, Matthias Elter"); | |||
_aboutData->addAuthor("Matthias Elter", 0, "[email protected]"); | |||
} | |||
KAboutApplication dialog(_aboutData); | |||
dialog.show(); | |||
} | |||
</syntaxhighlight> | |||
The implementation of the void about() action handler we reimplement from KPanelApplet creates a KAboutData object (see {{path|kdelibs/kdecore/kaboutdata.h}}) and makes use of KAboutApplication (see {{path|kdelibs/kdeui.kaboutapplication.h}}) to display a nice about dialog when the user selects "About" from the applet's context menu. | |||
== Screenshot of fifteenpieces 'about' dialog == | |||
[[Image:fifteenpieces_about.png]] | |||
Now let's have a look at the implementation of the game board class called PiecesTable As it inherits from QTableView you might want to have a look at the description of this abstract base class for tables in the excellent Qt documentation: [http://doc.trolltech.com/qtableview.html#details http://doc.trolltech.com/qtableview.html#details] | |||
<syntaxhighlight lang="cpp-qt" line> | |||
PiecesTable::PiecesTable(QWidget* parent, const char* name ) | |||
: QTableView(parent, name), _menu(0), _activeRow(-1), | |||
_activeCol(-1), _randomized(false) | |||
{ | |||
// setup table view | |||
setFrameStyle(StyledPanel | Sunken); | |||
setBackgroundMode(NoBackground); | |||
setMouseTracking(true); | |||
setNumRows(4); | |||
setNumCols(4); | |||
// init arrays | |||
initMap(); | |||
initColors(); | |||
} | |||
</syntaxhighlight> | |||
The constructor initializes a few member variables used by the game board, sets up the frame style of the table ( QTableView inherits from QFrame ), sets the background mode to NoBackground (we do this to avoid flicker), enables mouse tracking for the widget and configures the number of rows and columns of the table. We also call two protected initialization functions we have defined in PiecesTable to set up two arrays, one (array of ints) used to represent the game board and another (array of QColor ) where we store color values for the 15 pieces. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::initMap() | |||
{ | |||
_map.resize(16); | |||
for (unsigned int i = 0; i < 16; i++) | |||
_map[i] = i; | |||
_randomized = false; | |||
} | |||
</syntaxhighlight> | |||
initMap() resizes the QArray<int> we use to represent the game board to 16 (4x4 fields on the board) and initializes it with the values one to fifteen. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::initColors() | |||
{ | |||
_colors.resize(numRows() * numCols()); | |||
for (int r = 0; r < numRows(); r++) | |||
for (int c = 0; c < numCols(); c++) | |||
_colors[c + r *numCols()] = QColor(255 - 70 * c,255 - 70 * r, 150); | |||
} | |||
</syntaxhighlight> | |||
initColors() is similar but calculates different color values for each piece to increase the eye candy effect of our applet. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::paintCell(QPainter *p, int row, int col) | |||
{ | |||
int w = cellWidth(); | |||
int h = cellHeight(); | |||
int x2 = w - 1; | |||
int y2 = h - 1; | |||
int number = _map[col + row * numCols()] + 1; | |||
bool active = (row == _activeRow && col == _activeCol); | |||
// draw cell background | |||
if(number == 16) | |||
p->setBrush(colorGroup().background()); | |||
else | |||
p->setBrush(_colors[number-1]); | |||
p->setPen(NoPen); | |||
p->drawRect(0, 0, w, h); | |||
// draw borders | |||
if (height() > 40) { | |||
p->setPen(colorGroup().text()); | |||
if(col < numCols()-1) | |||
p->drawLine(x2, 0, x2, y2); // right border line | |||
if(row < numRows()-1) | |||
p->drawLine(0, y2, x2, y2); // bottom border line | |||
} | |||
// draw number | |||
if (number == 16) return; | |||
if(active) | |||
p->setPen(white); | |||
else | |||
p->setPen(black); | |||
p->drawText(0, 0, x2, y2, AlignHCenter | AlignVCenter, | |||
QString::number(number)); | |||
} | |||
</syntaxhighlight> | |||
We have reimplemented void paintCell() from QTableView to actually paint the table cells, which on our game board represent sliding pieces, when the widgets receives a paint event. The function is a bit to long to explain in all detail but is quite easy to understand looking up calls you are unsure about in the Qt documentation for {{qt2|QPainter}} and {{qt2|QTableView}} | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::resizeEvent(QResizeEvent *e) | |||
{ | |||
QTableView::resizeEvent(e); | |||
// set font | |||
QFont f = font(); | |||
if (height() > 50) | |||
f.setPixelSize(8); | |||
else if (height() > 40) | |||
f.setPixelSize(7); | |||
else if (height() > 24) | |||
f.setPixelSize(5); | |||
else | |||
f.setPixelSize(3); | |||
setFont(f); | |||
setCellWidth(contentsRect().width()/ numRows()); | |||
setCellHeight(contentsRect().height() / numCols()); | |||
} | |||
</syntaxhighlight> | |||
We also reimplement the resizeEvent() action handler of the QTableView widgets to adjust the game board according to the geometry we are resized to by the panel. You remember that the panel and thus also the applets can have different sizes and what we do in the resizeEvent is to adjust the font size used to draw on the widget (and thus used in the paintEvent to draw the piece numbers) to the widget size to increase readability on small panels. We furthermore set the cell width and height according to the widget geometry. The algorithm is simple as all cells (pieces) on our game board have the same size. | |||
For some extra eye candy we reimplement the mouseMoveEvent() action handler of the game board widget to highlight the cell (piece) under the mouse cursor if the mouse is moved over the board: | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::mouseMoveEvent(QMouseEvent* e) | |||
{ | |||
QTableView::mouseMoveEvent(e); | |||
// highlight on mouse over | |||
int row = findRow(e->y()); | |||
int col = findCol(e->x()); | |||
int oldrow = _activeRow; | |||
int oldcol = _activeCol; | |||
if(row >= numRows() | |||
|| col >= numCols() | |||
|| row < 0 | |||
|| col < 0) { | |||
_activeRow = -1; | |||
_activeCol = -1; | |||
} | |||
else { | |||
_activeRow = row; | |||
_activeCol = col; | |||
} | |||
updateCell(oldrow, oldcol, false); | |||
updateCell(row, col, false); | |||
} | |||
</syntaxhighlight> | |||
We are done now with drawing the game board and adjusting its size to game board geometry. While the applet already looks nice it is not of much use without the game logic for moving pieces around in place. | |||
== Screenshot of fifteenpieces applet == | |||
[[Image:fifteenpieces_applet.png]] | |||
As the idea of the game is to put randomized pieces in numerical order, we first implement a helper function used to randomize the arrangement of the pieces on the game board: | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::randomizeMap() | |||
{ | |||
QArray<int> positions; | |||
positions.fill(0, 16); | |||
for (unsigned int i = 0; i < 16; i++) { | |||
while(1) { | |||
int r = (int) (((double)rand() / RAND_MAX) * 16); | |||
if(positions[r] == 0) { | |||
_map[i] = r; | |||
positions[r] = 1; | |||
break; | |||
} | |||
} | |||
} | |||
repaint(); | |||
_randomized = true; | |||
} | |||
</syntaxhighlight> | |||
We need a second helper function to be called after each move operation to check whether the player won with the previous move and to display a message box in case of victory. You may have notice the _randomized member variable in the functions above and from the code in checkwin() you see that it is used to make sure that we trigger the "You win!" message box only if the player actually did randomize the game board before putting it into numerical order. | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::checkwin() | |||
{ | |||
if(!_randomized) return; | |||
int i; | |||
for (i = 0; i < 16; i++) | |||
if(i != _map[i]) | |||
break; | |||
if (i == 16) | |||
KMessageBox::information(this, | |||
i18n("Congratulations!\nYou win the game!"), | |||
i18n("Fifteen Pieces")); | |||
} | |||
</syntaxhighlight> | |||
Move operations are triggered by the player clicking on one of the sliding pieces. We reimplement the mousePressEvent() handler of the game board widgets to handle mouse clicks on the board. Right mouse button clicks trigger an additional context menu with actions to randomize or reset the game board. A left mouse button click triggers the game logic which is pretty simple if you take 5 minutes to think about it. You should be able to understand the details reading the numerous comments in the code: | |||
<syntaxhighlight lang="cpp-qt" line> | |||
void PiecesTable::mousePressEvent(QMouseEvent* e) | |||
{ | |||
QTableView::mousePressEvent(e); | |||
if (e->button() == RightButton) { | |||
// setup RMB pupup menu | |||
if(!_menu) { | |||
_menu = new QPopupMenu(this); | |||
_menu->insertItem(i18n("R&andomize Pieces"), mRandomize); | |||
_menu->insertItem(i18n("&Reset Pieces"), mReset); | |||
_menu->adjustSize(); | |||
} | |||
// execute RMB popup and check result | |||
switch(_menu->exec(mapToGlobal(e->pos()))) { | |||
case mRandomize: | |||
randomizeMap(); | |||
break; | |||
case mReset: | |||
initMap(); | |||
repaint(); | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
else { | |||
// GAME LOGIC | |||
// find the free position | |||
int pos = _map.find(15); | |||
if(pos < 0) return; | |||
int frow = pos / numCols(); | |||
int fcol = pos - frow * numCols(); | |||
// find click position | |||
int row = findRow(e->y()); | |||
int col = findCol(e->x()); | |||
// sanity check | |||
if (row < 0 || row >= numRows()) return; | |||
if (col < 0 || col >= numCols()) return; | |||
// valid move? | |||
if(row != frow && col != fcol) return; | |||
// rows match -> shift pieces | |||
if(row == frow) { | |||
if (col < fcol) { | |||
for(int c = fcol; c > col; c-) { | |||
_map[c + row * numCols()] = _map[ c-1 + row *numCols()]; | |||
updateCell(row, c, false); | |||
} | |||
} | |||
else if (col > fcol) { | |||
for(int c = fcol; c < col; c++) { | |||
_map[c + row * numCols()] = _map[ c+1 + row *numCols()]; | |||
updateCell(row, c, false); | |||
} | |||
} | |||
} | |||
// cols match -> shift pieces | |||
else if (col == fcol) { | |||
if (row < frow) { | |||
for(int r = frow; r > row; r-) { | |||
_map[col + r * numCols()] = _map[ col + (r-1) *numCols()]; | |||
updateCell(r, col, false); | |||
} | |||
} | |||
else if (row > frow) { | |||
for(int r = frow; r < row; r++) { | |||
_map[col + r * numCols()] = _map[ col + (r+1) *numCols()]; | |||
updateCell(r, col, false); | |||
} | |||
} | |||
} | |||
// move free cell to click position | |||
_map[col + row * numCols()] = 15; | |||
updateCell(row, col, false); | |||
// check if the player wins with this move | |||
checkwin(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
Ok, we are done, now either download and modify modify [http://developer.kde.org/documentation/tutorials/dot/khelloworldapplet.tar.gz khelloworldapplet.tar.gz] (308 kB) as described above or download [http://developer.kde.org/documentation/tutorials/dot/kfifteenapplet-1.0.tar.gz kfifteenapplet-1.0.tar.gz] (311 kB) to try the applet. I'm quite sure that you will agree that while the fifteen pieces applet is more or less useless it's a nifty toy to waste your time playing around with. You are good if you manage to figure out the basic move patterns used to win basically every game in less than 10min. ;-) | |||
== Screenshot of popup menu above applet == | |||
Now that we have implemented a demo applet I'm going to discuss the "advanced" API features we did not make use of for the simple fifteen pieces applet. | |||
== Applets of variable size == | |||
Some panel applets like the pager applet need variable sizes depending on their state (for example the number of desktops for the pager applet). To change the width or height of your applet simply emit the signal void updateLayout() (see {{path|kpanelapplet.h}}) and kicker will relayout the panel and requery widthForHeight() (horizontal panel) or heightForWidth() | |||
Session management | |||
Unlike normal KDE applications panel applets are not managed by ksmserver, the KDE session manager, but saved and restored by the panel. Session management is transparent to the applet. All that needs to be done to implement functional session management for your applet is to ensure you _always_ use the KConfig object returned by KPanelApplet::config() to save and read applet configuration settings. For unique applets ( X-KDE-UniqueApplet=true ) this config object will write settings to {{path|$KDEDIR/share/config/appletidrc}} with "appletid" being the library name as defined in the {{path|.desktop}} file with X-KDE-Library For non-unique applets the name of the config file will contain a unique session id. | |||
== Stretch applets == | |||
Both the hello world and fifteen pieces applet we have discussed are of type KPanelApplet::Normal as passed in the applet constructor to the KPanelApplet constructor. However there is a second type KPanelApplet::Stretch that is used for stretch applets like the taskbar applet that comes with kicker. For stretch applets the values returned by widthForHeight() and heightForWidth() are interpreted as minimal size values by kicker. This means a stretch applet will never be smaller than the size value you choose, but will be stretched by kicker to take up all the available space to the next applet on the panel. Move around the taskbar applet on the panel to see this effect. | |||
== General action dispatcher == | |||
Looking at the class declaration of KPanelApplet you may have noticed that there is one value ( ReportBug ) in the Action enumeration which does not have a virtual action handler function. The reason for this is ReportBug was added after the release of KDE 2.0 and it is not possible to add virtual functions without breaking binary compatibility of the KDE 2 platform. For this reason to handle the ReportBug action you must reimplement the general action dispatcher function void action(Action) It is called when the user selects any of the actions from the applet's context menu. | |||
== Requesting keyboard focus == | |||
The panel normally does not accept keyboard focus. For this reason applets that need keyboard focus (for example to input text) have to emit the void requestFocus() signal (for example as reaction to a mouse click event on the applet) to retrieve keyboard focus. | |||
== Applet GUI layout == | |||
As mentioned earlier the panel can have different orientations (horizontal or vertical) and can be placed on different borders of the desktop. Thus you might want to adjust the GUI layout of your applet depending on the panel orientation. There are two protected functions declared in KPanelApplet for applets to query the panel orientation and the direction in which popup menus should be positioned depending on the panel position. The functions are Orientation orientation() and Direction popupDirection() There are also two virtual handlers ( void orientationChange() and void popupDirectionChange() ) an applet can reimplement to react on orientation or direction changes. | |||
== Tips 'n' tricks == | |||
Never call show() in the applet's constructor. There is absolutely no reason to call show() in a widget constructor. The reason why this is important for panel applets is that QXEmbed ({{path|kdelibs/kdecore/qxembed.h}}), the class used to embed external applets (those run via the proxy) into kicker, often suffers from race conditions when a window is visible before it is reparented. | |||
As panel applets provide but a very small GUI it is often easier to do the layouting by hand in the resizeEvent() instead of using QLayout. |
Latest revision as of 20:52, 29 June 2011
Introduction
The KDE panel (below referred to as "kicker") has been rewritten from scratch for KDE 2.0 and one of the main goals of the rewrite was to increase extensibility. For this reason with KDE 2.0 an API for panel applets has been introduced followed by an API for panel extension introduced with the KDE 2.1 release. Panel applets and extensions are simply referred to as "plugins" in the following paragraphs.
Panel applets are small applications living inside the panel. Available applets range from desktop pagers and task bars to little games or other toys. In contrast to applets the panel extension API is an interface for extensions living outside of the panel, in the window manager dock area (as defined in the freedesktop.org window manager specifications and are managed by the panel. Examples for panel extensions are the external task bar and the dock application bar which adds support for Window Maker applets and other applications using the standard X11 docking mechanism to kicker.
Starting with a short technology overview this tutorial discusses the implementation of a simple panel applet. Both the applet and extension APIs are simple. This qualifies writing a panel applet as a suitable task for an introduction to KDE programming. This tutorial presumes that the reader has some basic C++ and Qt knowledge. You can also start with a basic Qt2 tutorial.
Overview
Panel plugins are implemented as DSO s (Dynamic Shared Objects). The panel locates available applets by searching for applet description files in (ALL_KDEDIRS)/share/apps/kicker/applets. Every plugin must install a description file to be recognized by the panel. Available panel extensions are located by searching (A_KDEDIR)/share/apps/kicker/extensions for similar description files.
Implementing a panel applet is as easy as inheriting from the base class KPanelApplet (declared in kdelibs/kdeui/kpanelapplet.h), providing a factory function and installing the description file mentioned above. For extensions the respective base class is KPanelExtension, declared in kdelibs/kdeui/kpanelextension.h.
While plugins are implemented as shared libraries and thus loaded into the panel's address space, for security and stability reasons kicker also implements loading of plugins via external proxy processes called appletproxy and extensionproxy. The loading behavior is user configurable via kcontrol, with the obvious benefit that users may configure their panel to load untrusted third party plugins via the proxies to avoid buggy plugins taking the whole panel down with them.
While as an plugin author you don't have to be concerned about these implementation details it is good to know what happens behind the scenes. Therefore I might mention here that the panel communicates with the proxies via the DCOP protocol (see kdelibs/dcop) and makes use of QXEmbed (see kdelibs/kdeui/qxembed.h) to embed external (loaded via one of the two proxies) plugins into the panel.
A hello world panel applet
We start with a hello world panel applet that can be implemented in only a few lines of code and is a good base upon which to build a more complex applet. As already mentioned in the overview section all that needs to be done is inheriting from KPanelApplet, providing a factory function and a .desktop description file.
Our little project consists of four files: helloworldapplet.h containing the class declaration, helloworldapplet.cpp containing the class implementation and the factory function, helloworldapplet.desktop the description file and Makefile.am as input for the automake/autoconf KDE build system.
HelloWorldApplet class declaration
class HelloWorldApplet : public KPanelApplet
{
Q_OBJECT
public:
HelloWorldApplet( const QString& configFile, Type t = Stretch,
int actions = 0, QWidget *parent = 0,
const char *name = 0 );
int widthForHeight( int height ) const;
int heightForWidth( int width ) const;
};
Having a look at kpanelapplet.h see that all we do is re-implement the constant virtual functions int widthForHeight(int) and heightForWidth(int) Playing around with kicker you may notice that you can arrange the panel either horizontally or vertically at the desktop borders. You will also notice that there is a fixed set of panel sizes (representing panel height for horizontal panels and width for vertical panels) to choose from.
The concept of panel applets is that one size component is dictated by the panel while the other one is free for the applet to choose. On horizontal panels the applet's height is fixed according to the panel height while the applet is free to choose its width. Respectively on vertical panels the applet's height is fixed while it is free to choose its height.
Every panel applet should be prepared to be used both on horizontal and vertical panels. The panel uses the two functions we are re-implementing for the HelloWorldApplet to query the applet's preferred size. A horizontal panel will call widthForHeight() while a vertical panel will call heightForWidth() to query the free size component of the applet. Applets are guaranteed to be resized according to the size they request.
HelloWorldApplet class implementation
HelloWorldApplet::HelloWorldApplet( const QString& configFile,
Type type, int actions,
QWidget *parent, const char *name )
: KPanelApplet( configFile, type, actions, parent, name )
{
setBackgroundColor( blue );
setFrameStyle( StyledPanel | Sunken );
}
int HelloWorldApplet::widthForHeight( int h ) const
{
return h; // we want to be quadratic
}
int HelloWorldApplet::heightForWidth( int w ) const
{
return w; // we want to be quadratic
}
Our constructor simply passes its default arguments to the KPanelApplet constructor, sets the frame style to StyledPanel Sunken (see qframe.h) and sets the background color of the applet to blue so we recognize it later running in the panel. Both widthForHeight() and heightForHeight() are implemented to choose a quadratic geometry for the applet. Thus we expect our applet to show up as a sunken blue square on the panel when run.
Screenshot of 'Hello, World!' applet
The factory function
extern "C"
{
KPanelApplet* init( QWidget *parent, const QString& configFile )
{
KGlobal::locale()->insertCatalogue( "helloworldapplet");
return new HelloWorldApplet( configFile, KPanelApplet::Normal,
0, parent, "helloworldapplet");
}
}
The factory function is mostly copy&paste work where you would replace "helloworldapplet" with "myapplet" to adjust it to your custom applet.
The description file: helloworldapplet.desktop
[Desktop Entry]
Name = Hello World
Comment = Hello World Applet
X-KDE-Library = libhelloworldapplet
X-KDE-UniqueApplet = true
Besides standard .desktop file keys like "Name", "Comment" and "Icon" there are two panel applet specific keys:
X-KDE-Library
is used by the panel to locate the applet DSO (Dynamic Shared Object)
Example: X-KDE-Library=libexampleapplet
X-KDE-UniqueApplet
Similar to KApplication and KUniqueApplication there are two types of panel applets. Use unique applets when it makes no sense to run more than one instance of an applet in the panel. A good example for unique applets is the taskbar applet. Use normal applets when you need instance specific configuration. An example is the koolclock applet where you might want to run two instances in your panel, one configured as analog clock, the other one as digital clock. X-KDE-UniqueApplet is a boolean key which defaults to "false".
Example: X-KDE-UniqueApplet=true
The following conventions are used for the applet
DSOs:
Name
libappletnameapplet.la
LDFLAGS
-module -no-undefined
The automake input file: Makefile.am
INCLUDES = $(all_includes)
lib_LTLIBRARIES = libhelloworldapplet.la
libhelloworldapplet_la_SOURCES = helloworldapplet.cpp
METASOURCES = AUTO
noinst_HEADERS = helloworldapplet.h
lnkdir = $(kde_datadir)/kicker/applets
lnk_DATA = helloworldapplet.desktop
EXTRA_DIST = $(lnk_DATA)
libhelloworldapplet_la_LDFLAGS = $(all_libraries) -version-info 1:0:0 -module \
-no-undefined
libhelloworldapplet_la_LIBADD = $(LIB_KDEUI)
messages:
$(XGETTEXT) *.cpp *.h -o $(podir)/helloworldapplet.pot
Explaining the details of this particular automake input file and the KDE build system in general is not the goal of this tutorial. So to adjust it to your applet project simply replace all occurrences of "helloworldapplet" with "myapplet".
Building and installing
You can download a complete helloworldapplet.tar.gz tarball here: khelloworldapplet.tar.gz (308 kB), which in addition to the four files mentioned above contains the automake/autoconf magic of the KDE build system. It's convenient to base your own panel applets on as the build system magic is already in place. Untar it, change into the untarred directory and build and install it with:
./configure -prefix=<your-kde-dir>
make
su -c 'make install'
Hello world!
Now that your applet is installed and the description file in place, a "Hello World" entry will show up in the Add-Applet sub menu of the panel menu. Select the "Hello World" entry and you will see our blue sunken quadratic panel applet show up in the panel. You will also notice a small grey handle on the left side of the applet. Use the right mouse button context menu of the applet handle to move or remove the applet. You can also move applets by dragging the handle.
Fifteen pieces panel applet
Searching for a panel applet suitable for this tutorial I stumbled over a screenshot of an applet implementation of the old fifteen pieces game for another desktop environment. While it's more a toy than of any real use it is simple and fast to implement and thus perfect for a tutorial. I'm sure you know the fifteen pieces game with the goal to put fifteen sliding pieces into numerical oder. The idea of the game is that on a 4x4 cell quadratic game board fifteen numbered (1 to 15) quadratic pieces must be put into numerical order while pieces can only be moved horizontally or vertically and with only one free cell to perform move operations on.
We are going to use a customized QTableView widget for the game board. Similar to the hello world applet our little project will consist of three files, fiftenapplet.cpp, fifteenapplet.h, fifteenapplet.desktop and the Makefile.am file.
fifteenapplet.h contains the declaration of FifteenApplet, the class inheriting from KPanelApplet :
class FifteenApplet : public KPanelApplet
{
Q_OBJECT
public:
FifteenApplet(const QString& configFile, Type t = Stretch,
int actions = 0,
QWidget *parent = 0, const char *name = 0);
int widthForHeight(int height) const;
int heightForWidth(int width) const;
void about();
private:
PiecesTable *_table;
KAboutData *_aboutData;
};
While it is very similar to the declaration of HelloWorldApplet, we have added a reimplementation of void about() (as defined in kpanelapplet.h) and private pointers for our game board class and a KAboutData object (see kdelibs/kdecore/kaboutdata.h) used to build an about dialog from.
Searching for void about() in the KPanelApplet class declaration you will find two similar protected functions void help() and void preferences() In the right mouse button context menu of some applets you find in addition to the already mentioned "Move" and "Remove" entries, entries called "About", "Help" and "Preferences". The three protected functions are action handlers called when a user selects "About", "Help" or "Preferences" from the applets context menu. You have to reimplement them in your applet class to handle them. Not every applet implements for example the "Preferences" action because it might not have any preferences to configure. To avoid unused menu actions in the applet's context menu, kicker will only display those that you configure it to do by passing the according actions you reimplement or'ed together as the third parameter of the applet's class constructor.
The second class declared in fifteenapplet.h is our game board inheriting from QTableView :
class PiecesTable : public QTableView
{
Q_OBJECT
public:
PiecesTable(QWidget* parent = 0, const char* name = 0);
protected:
void resizeEvent(QResizeEvent*);
void mousePressEvent(QMouseEvent*);
void mouseMoveEvent(QMouseEvent*);
void paintCell(QPainter *, int row, int col);
void initMap();
void initColors();
void randomizeMap();
void checkwin();
private:
QArray<int> _map;
QArray<QColor> _colors;
QPopupMenu *_menu;
int _activeRow, _activeCol;
bool _randomized;
enum MenuOp { mRandomize = 1, mReset = 2 };
};
I'm going to explain the details of this class below by means of the class implementation.
The factory function is very similar to the one of the helloworld applet with all occurrences of "helloworldapplet" replaced with "fifteenapplet". The second difference you might notice is that we pass KPanelApplet::About as the third parameter of the FifteenApplet constructor to make sure the "About" context menu entry will be there:
extern "C"
{
KPanelApplet* init(QWidget *parent, const QString& configFile)
{
KGlobal::locale()->insertCatalogue("kfifteenapplet");
return new FifteenApplet(configFile, KPanelApplet::Normal,
KPanelApplet::About, parent, "kfifteenapplet");
}
}
The implementation of the FifteenApplet class is very short as the game board class will handle all the drawing and game logic.
FifteenApplet::FifteenApplet(const QString& configFile, Type type, int actions,
QWidget *parent, const char *name)
: KPanelApplet(configFile, type, actions, parent, name), _aboutData(0)
{
// setup table
_table = new PiecesTable(this);
// setup layout
QHBoxLayout *_layout = new QHBoxLayout(this);
_layout->add(_table);
srand(time(0));
}
The constructor creates an instance of our game board class and places it into a simple layout object to resize it to the full applet size every time the applet itself is resized. srandom is used to initialize the random number generator with the current time in seconds (see the man pages of srandom and random) as seed.
int FifteenApplet::widthForHeight(int h) const
{
return h; // we want to be quadratic
}
int FifteenApplet::heightForWidth(int w) const
{
return w; // we want to be quadratic
}
Similar to the hello world applet the fifteen pieces applet will have a quadratic shape.
void FifteenApplet::about()
{
if(!_aboutData) {
_aboutData = new KAboutData("kfifteenapplet", I18N_NOOP("KFifteenApplet"),
"1.0", I18N_NOOP("Fifteen pieces applet.\n\n"
"The goal is to put the sliding pieces into numerical order.\n"
"Select \"Randomize Pieces\" from the RMB menu to start a game."),
KAboutData::License_BSD, "(c) 2001, Matthias Elter");
_aboutData->addAuthor("Matthias Elter", 0, "[email protected]");
}
KAboutApplication dialog(_aboutData);
dialog.show();
}
The implementation of the void about() action handler we reimplement from KPanelApplet creates a KAboutData object (see kdelibs/kdecore/kaboutdata.h) and makes use of KAboutApplication (see kdelibs/kdeui.kaboutapplication.h) to display a nice about dialog when the user selects "About" from the applet's context menu.
Screenshot of fifteenpieces 'about' dialog
Now let's have a look at the implementation of the game board class called PiecesTable As it inherits from QTableView you might want to have a look at the description of this abstract base class for tables in the excellent Qt documentation: http://doc.trolltech.com/qtableview.html#details
PiecesTable::PiecesTable(QWidget* parent, const char* name )
: QTableView(parent, name), _menu(0), _activeRow(-1),
_activeCol(-1), _randomized(false)
{
// setup table view
setFrameStyle(StyledPanel | Sunken);
setBackgroundMode(NoBackground);
setMouseTracking(true);
setNumRows(4);
setNumCols(4);
// init arrays
initMap();
initColors();
}
The constructor initializes a few member variables used by the game board, sets up the frame style of the table ( QTableView inherits from QFrame ), sets the background mode to NoBackground (we do this to avoid flicker), enables mouse tracking for the widget and configures the number of rows and columns of the table. We also call two protected initialization functions we have defined in PiecesTable to set up two arrays, one (array of ints) used to represent the game board and another (array of QColor ) where we store color values for the 15 pieces.
void PiecesTable::initMap()
{
_map.resize(16);
for (unsigned int i = 0; i < 16; i++)
_map[i] = i;
_randomized = false;
}
initMap() resizes the QArray<int> we use to represent the game board to 16 (4x4 fields on the board) and initializes it with the values one to fifteen.
void PiecesTable::initColors()
{
_colors.resize(numRows() * numCols());
for (int r = 0; r < numRows(); r++)
for (int c = 0; c < numCols(); c++)
_colors[c + r *numCols()] = QColor(255 - 70 * c,255 - 70 * r, 150);
}
initColors() is similar but calculates different color values for each piece to increase the eye candy effect of our applet.
void PiecesTable::paintCell(QPainter *p, int row, int col)
{
int w = cellWidth();
int h = cellHeight();
int x2 = w - 1;
int y2 = h - 1;
int number = _map[col + row * numCols()] + 1;
bool active = (row == _activeRow && col == _activeCol);
// draw cell background
if(number == 16)
p->setBrush(colorGroup().background());
else
p->setBrush(_colors[number-1]);
p->setPen(NoPen);
p->drawRect(0, 0, w, h);
// draw borders
if (height() > 40) {
p->setPen(colorGroup().text());
if(col < numCols()-1)
p->drawLine(x2, 0, x2, y2); // right border line
if(row < numRows()-1)
p->drawLine(0, y2, x2, y2); // bottom border line
}
// draw number
if (number == 16) return;
if(active)
p->setPen(white);
else
p->setPen(black);
p->drawText(0, 0, x2, y2, AlignHCenter | AlignVCenter,
QString::number(number));
}
We have reimplemented void paintCell() from QTableView to actually paint the table cells, which on our game board represent sliding pieces, when the widgets receives a paint event. The function is a bit to long to explain in all detail but is quite easy to understand looking up calls you are unsure about in the Qt documentation for QPainter and QTableView
void PiecesTable::resizeEvent(QResizeEvent *e)
{
QTableView::resizeEvent(e);
// set font
QFont f = font();
if (height() > 50)
f.setPixelSize(8);
else if (height() > 40)
f.setPixelSize(7);
else if (height() > 24)
f.setPixelSize(5);
else
f.setPixelSize(3);
setFont(f);
setCellWidth(contentsRect().width()/ numRows());
setCellHeight(contentsRect().height() / numCols());
}
We also reimplement the resizeEvent() action handler of the QTableView widgets to adjust the game board according to the geometry we are resized to by the panel. You remember that the panel and thus also the applets can have different sizes and what we do in the resizeEvent is to adjust the font size used to draw on the widget (and thus used in the paintEvent to draw the piece numbers) to the widget size to increase readability on small panels. We furthermore set the cell width and height according to the widget geometry. The algorithm is simple as all cells (pieces) on our game board have the same size.
For some extra eye candy we reimplement the mouseMoveEvent() action handler of the game board widget to highlight the cell (piece) under the mouse cursor if the mouse is moved over the board:
void PiecesTable::mouseMoveEvent(QMouseEvent* e)
{
QTableView::mouseMoveEvent(e);
// highlight on mouse over
int row = findRow(e->y());
int col = findCol(e->x());
int oldrow = _activeRow;
int oldcol = _activeCol;
if(row >= numRows()
|| col >= numCols()
|| row < 0
|| col < 0) {
_activeRow = -1;
_activeCol = -1;
}
else {
_activeRow = row;
_activeCol = col;
}
updateCell(oldrow, oldcol, false);
updateCell(row, col, false);
}
We are done now with drawing the game board and adjusting its size to game board geometry. While the applet already looks nice it is not of much use without the game logic for moving pieces around in place.
Screenshot of fifteenpieces applet
As the idea of the game is to put randomized pieces in numerical order, we first implement a helper function used to randomize the arrangement of the pieces on the game board:
void PiecesTable::randomizeMap()
{
QArray<int> positions;
positions.fill(0, 16);
for (unsigned int i = 0; i < 16; i++) {
while(1) {
int r = (int) (((double)rand() / RAND_MAX) * 16);
if(positions[r] == 0) {
_map[i] = r;
positions[r] = 1;
break;
}
}
}
repaint();
_randomized = true;
}
We need a second helper function to be called after each move operation to check whether the player won with the previous move and to display a message box in case of victory. You may have notice the _randomized member variable in the functions above and from the code in checkwin() you see that it is used to make sure that we trigger the "You win!" message box only if the player actually did randomize the game board before putting it into numerical order.
void PiecesTable::checkwin()
{
if(!_randomized) return;
int i;
for (i = 0; i < 16; i++)
if(i != _map[i])
break;
if (i == 16)
KMessageBox::information(this,
i18n("Congratulations!\nYou win the game!"),
i18n("Fifteen Pieces"));
}
Move operations are triggered by the player clicking on one of the sliding pieces. We reimplement the mousePressEvent() handler of the game board widgets to handle mouse clicks on the board. Right mouse button clicks trigger an additional context menu with actions to randomize or reset the game board. A left mouse button click triggers the game logic which is pretty simple if you take 5 minutes to think about it. You should be able to understand the details reading the numerous comments in the code:
void PiecesTable::mousePressEvent(QMouseEvent* e)
{
QTableView::mousePressEvent(e);
if (e->button() == RightButton) {
// setup RMB pupup menu
if(!_menu) {
_menu = new QPopupMenu(this);
_menu->insertItem(i18n("R&andomize Pieces"), mRandomize);
_menu->insertItem(i18n("&Reset Pieces"), mReset);
_menu->adjustSize();
}
// execute RMB popup and check result
switch(_menu->exec(mapToGlobal(e->pos()))) {
case mRandomize:
randomizeMap();
break;
case mReset:
initMap();
repaint();
break;
default:
break;
}
}
else {
// GAME LOGIC
// find the free position
int pos = _map.find(15);
if(pos < 0) return;
int frow = pos / numCols();
int fcol = pos - frow * numCols();
// find click position
int row = findRow(e->y());
int col = findCol(e->x());
// sanity check
if (row < 0 || row >= numRows()) return;
if (col < 0 || col >= numCols()) return;
// valid move?
if(row != frow && col != fcol) return;
// rows match -> shift pieces
if(row == frow) {
if (col < fcol) {
for(int c = fcol; c > col; c-) {
_map[c + row * numCols()] = _map[ c-1 + row *numCols()];
updateCell(row, c, false);
}
}
else if (col > fcol) {
for(int c = fcol; c < col; c++) {
_map[c + row * numCols()] = _map[ c+1 + row *numCols()];
updateCell(row, c, false);
}
}
}
// cols match -> shift pieces
else if (col == fcol) {
if (row < frow) {
for(int r = frow; r > row; r-) {
_map[col + r * numCols()] = _map[ col + (r-1) *numCols()];
updateCell(r, col, false);
}
}
else if (row > frow) {
for(int r = frow; r < row; r++) {
_map[col + r * numCols()] = _map[ col + (r+1) *numCols()];
updateCell(r, col, false);
}
}
}
// move free cell to click position
_map[col + row * numCols()] = 15;
updateCell(row, col, false);
// check if the player wins with this move
checkwin();
}
}
Ok, we are done, now either download and modify modify khelloworldapplet.tar.gz (308 kB) as described above or download kfifteenapplet-1.0.tar.gz (311 kB) to try the applet. I'm quite sure that you will agree that while the fifteen pieces applet is more or less useless it's a nifty toy to waste your time playing around with. You are good if you manage to figure out the basic move patterns used to win basically every game in less than 10min. ;-)
Now that we have implemented a demo applet I'm going to discuss the "advanced" API features we did not make use of for the simple fifteen pieces applet.
Applets of variable size
Some panel applets like the pager applet need variable sizes depending on their state (for example the number of desktops for the pager applet). To change the width or height of your applet simply emit the signal void updateLayout() (see kpanelapplet.h) and kicker will relayout the panel and requery widthForHeight() (horizontal panel) or heightForWidth() Session management Unlike normal KDE applications panel applets are not managed by ksmserver, the KDE session manager, but saved and restored by the panel. Session management is transparent to the applet. All that needs to be done to implement functional session management for your applet is to ensure you _always_ use the KConfig object returned by KPanelApplet::config() to save and read applet configuration settings. For unique applets ( X-KDE-UniqueApplet=true ) this config object will write settings to $KDEDIR/share/config/appletidrc with "appletid" being the library name as defined in the .desktop file with X-KDE-Library For non-unique applets the name of the config file will contain a unique session id.
Stretch applets
Both the hello world and fifteen pieces applet we have discussed are of type KPanelApplet::Normal as passed in the applet constructor to the KPanelApplet constructor. However there is a second type KPanelApplet::Stretch that is used for stretch applets like the taskbar applet that comes with kicker. For stretch applets the values returned by widthForHeight() and heightForWidth() are interpreted as minimal size values by kicker. This means a stretch applet will never be smaller than the size value you choose, but will be stretched by kicker to take up all the available space to the next applet on the panel. Move around the taskbar applet on the panel to see this effect.
General action dispatcher
Looking at the class declaration of KPanelApplet you may have noticed that there is one value ( ReportBug ) in the Action enumeration which does not have a virtual action handler function. The reason for this is ReportBug was added after the release of KDE 2.0 and it is not possible to add virtual functions without breaking binary compatibility of the KDE 2 platform. For this reason to handle the ReportBug action you must reimplement the general action dispatcher function void action(Action) It is called when the user selects any of the actions from the applet's context menu.
Requesting keyboard focus
The panel normally does not accept keyboard focus. For this reason applets that need keyboard focus (for example to input text) have to emit the void requestFocus() signal (for example as reaction to a mouse click event on the applet) to retrieve keyboard focus.
Applet GUI layout
As mentioned earlier the panel can have different orientations (horizontal or vertical) and can be placed on different borders of the desktop. Thus you might want to adjust the GUI layout of your applet depending on the panel orientation. There are two protected functions declared in KPanelApplet for applets to query the panel orientation and the direction in which popup menus should be positioned depending on the panel position. The functions are Orientation orientation() and Direction popupDirection() There are also two virtual handlers ( void orientationChange() and void popupDirectionChange() ) an applet can reimplement to react on orientation or direction changes.
Tips 'n' tricks
Never call show() in the applet's constructor. There is absolutely no reason to call show() in a widget constructor. The reason why this is important for panel applets is that QXEmbed (kdelibs/kdecore/qxembed.h), the class used to embed external applets (those run via the proxy) into kicker, often suffers from race conditions when a window is visible before it is reparented.
As panel applets provide but a very small GUI it is often easier to do the layouting by hand in the resizeEvent() instead of using QLayout.