Dieser und noch weitere Artikel wurde von phlox81 erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


1 Vorbemerkungen

Nachdem ich im ersten Artikel die Grundlagen von wxWidgets behandelt habe, möchte ich in diesem Artikel die Grundlagen etwas vertiefen, und an einer kleinen Beispielanwendung zeigen, was man mit wxWidgets so realisieren kann. Diese Beispielanwendung wird ein Tic-Tac-Toe Spiel sein. Im Gegensatz zum ersten Artikel verwende ich nun jedoch eine neue wxWidgets-Version(2.8.0), welche aber kompatibel zu 2.6 ist, somit sollte sich die Beispielanwendung auch mit einer älteren wxWidgets-Version kompilieren lassen.

1.1 wxWidgets 2.8.0

Bevor ich mit dem eigentlichen Tutorial anfange, möchte ich kurz noch auf einige Änderungen und Neuerungen hinweisen, die wxWidgets 2.8.0 mit sich bringt. wxWidgets verfügt nun über eine eigene RichTextCtrl-Library, für die Ein- bzw. Ausgabe von formatiertem Text (Allerdings kann diese Library noch keine Standard rtf-Dateien lesen).
Auch wurde eine Dockinglibrary in wxWidgets aufgenommen, mit wxAUI (Advanced User Interface) lassen sich Fenster erstellen, welche sich in der Anwendung dann verschieben und an/abdocken lassen. Alle Neuerungen finden sich in diesem Artikel.

Ein kleiner Hinweis noch an alle MinGW-Benutzer, die bisher die Library mit Msys gebaut haben:
Die mitgelieferten Builddateien scheinen fehlerhaft zu sein, ich konnte jedenfalls wxWidgets2.8.0 nicht mit Msys kompilieren. Alternativ geht es aber über die Windowskommandozeile, einfach in das Verzeichnis %wxDir%/build/msw wechseln, und mit "mingw32-make.exe -f makefile.gcc" den Buildprozess starten. Wer die Library als Releaseversion bauen will, muss in der config.gcc den Wert BUILD von debug auf release setzen.

2 Implementierung eines eigenen Steuerelementes

Beim Erstellen eines eigenen Steuerelementes ist vor allem das Eventhandling wichtig. Man muss in seinem Steuerelement bestimmte Events empfangen können, und auf diese dann entsprechend reagieren. Falls es schon ein Steuerelement gibt, welches einen Teil der Anforderungen abdeckt, empfiehlt es sich von diesem abzuleiten. Wenn dies nicht möglich ist, sollte man sein Steuerelement von wxPanel ableiten, oder falls man ein "scrollbares" Steuerelement will, von wxScrolledWindow.
Bei der Beispielanwendung ist es so, dass wir ein Steuerelement brauchen, welches als Spielfeld, bzw. Spielstein fungiert.
Dafür leiten wir von wxPanel ab, und richten für wxEVT_PAINT und wxEVT_LEFT_DOWN eigene Handler ein:

2.1 Selber zeichnen

Für den Spielstein ist es wichtig, anhand seines Stati ein X oder ein O zu zeichnen. Wenn man den wxPaintEvent überschreibt, muss man in der OnPaint-Methode einen wxPaintDC anlegen, damit wxWidgets weiß, dass das Fenster neugezeichnet wurde, ansonsten würde wxWidgets weiter Paintevents an das Fenster schicken.

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void FieldPanel::OnPaint(wxPaintEvent& event)
{
    wxPaintDC dc(this);
    dc.DrawRectangle(2,2,GetSize().GetWidth()-2,GetSize().GetHeight()-2);
    int abstand = 10;
    if(status == GameLogic::X)
    {
        wxPen pen = *wxRED_PEN;
        pen.SetStyle(wxSOLID);
        pen.SetWidth(4);
        dc.SetPen(pen);
        dc.DrawLine(abstand,abstand,GetSize().GetWidth()-abstand,GetSize().GetHeight()-abstand);
        dc.DrawLine(GetSize().GetWidth()-abstand,abstand,abstand,GetSize().GetHeight()-abstand);
    }
    else if(status == GameLogic::O)
    {
        wxPen pen(wxColour(0,0,250),4);
        dc.SetPen(pen);
        int x = GetSize().GetWidth()/2;
        int y = GetSize().GetHeight()/2;
        if( x < y )
            dc.DrawCircle(x,y,x - abstand);
        else
            dc.DrawCircle(x,y,y - abstand);
    }
}


2.2 Mausevent

Da nur der Linksklick als Mausevent registriert wurde, reagiert auch das Steuerelement nur auf diesen.
In diesem Fall muss geprüft werden, ob der aktuelle Status noch den Anfangswert ist, und dann ein neuer Wert gesetzt werden:
C++:
1
2
3
4
5
6
7
8
void FieldPanel::OnLeftClick(wxMouseEvent& event)
{
    if(status == GameLogic::EMPTY)
    {
        status = GameLogic::X;// Der Spieler hat den X Stein.
        Refresh();// Panel aktualisieren
    }
}


Jetzt stellt das Panel zwar den Zug dar, aber außerhalb von dieser Panelinstanz hat niemand etwas von diesen Event wahrgenommen. Wenn wir davon ausgehen, das jeder Spielstein einem Spielfeld liegt, so müsste dieses Spielfeld das Parent des Steuerelementes sein. Man könnte also mittels eines Casts den Parentzeiger auf die Spielfeldklasse casten, und dort dann eine Methode aufrufen. Für den aktuellen Fall würde dies gehen, solange bis das Control eine andere Parentklasse hätte.
Es wäre also eleganter wenn das Steuerelement einen Event an sein Parentfenster senden könnte.

2.3 Ein eigener Notifyevent

Die Implementierung unseres eigenen Events sieht so aus:
C++:
1
2
3
4
5
6
7
8
9
10
class TurnEvent : public wxNotifyEvent
{
    int status;
    int field;
public:
    TurnEvent(int status, int field, wxEventType eventType = wxEVT_NULL, int id = 0);
    TurnEvent(const TurnEvent& copy);
    virtual ~TurnEvent();
    virtual TurnEvent* Clone()const {return new TurnEvent(*this);};
};


Über die Variablen status und field wird übergeben welches Spielfeld den Event auslöst, und welcher Spielstein dort gesetzt werden soll.
Damit wxWidgets die Eventklasse auch kennt, und den Event entsprechend weiterleiten kann, sind noch einige Makros nötig:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// in der TurnEvent.hpp (nach der Klassendeklaration)
 
// ein Typedef auf die entsprechenden EventHandler
typedef void(wxEvtHandler::*TurnEventFunction)(TurnEvent&);
 
// wxWidgets den neuen Event mitteilen
BEGIN_DECLARE_EVENT_TYPES()
  DECLARE_EVENT_TYPE(EVT_TURN_CHANGE, -1)
END_DECLARE_EVENT_TYPES()
 
// dieses #define sorgt dafür das man den Event über den Makro EventTable einbinden kann
#define EVT_TURN_CHANGE_MACRO(id, fn) DECLARE_EVENT_TABLE_ENTRY(               \
        EVT_TURN_CHANGE, id, -1, (wxObjectEventFunction)               \
        (wxEventFunction)(TurnEventFunction) & fn,                             \
        (wxObject*) NULL),
 
// dieses #define sorgt dafür das man den Event auch über Connect verbinden kann
#define TurnEventHandler(func)                                                 \
        (wxObjectEventFunction)                                                \
        (wxEventFunction)wxStaticCastEvent(TurnEventFunction, &func)
 
// in der TurnEvent.cpp muss nun 'nur' noch der Event Definiert werden:
DEFINE_EVENT_TYPE(EVT_TURN_CHANGE)


Nun ist der Event fertig implementiert, und kann nun von FieldPanel in der OnLeftClick Methode gesendet werden:
C++:
1
2
3
4
5
6
7
8
9
10
void FieldPanel::OnLeftClick(wxMouseEvent& event)
{
    if(status == GameLogic::EMPTY)
    {
        status = GameLogic::X;
        Refresh();// Panel aktualisieren
        TurnEvent myevent(GameLogic::X,id,EVT_TURN_CHANGE);
        GetParent()->AddPendingEvent(myevent);
    }
}


... und wird in der Klasse GamePanel empfangen:
C++:
1
2
3
4
5
6
7
8
// im Konstruktor wird Connect aufgerufen
Connect(EVT_TURN_CHANGE,TurnEventHandler(GamePanel::OnTurnChange));
 
// Und in der GamePanel.cpp entsprechend die Event Methode definiert:
void GamePanel::OnTurnChange(TurnEvent& event)
{
    wxMessageBox("TurnChange Event!");
}


Nun ist unser eigenes Steuerelement fertig.

3 Das Programm

Mit Fieldpanel verfügt die Anwendung nun über ein Steuerelement welches als Spielstein fungieren kann. Das Programm hat einen recht einfachen Aufbau, als Basisgerüst dient eine von wxFrame abgeleitete Klasse, welches ja schon aus dem ersten Teil des Tutorials bekannt ist. In unserer Anwendung ist die Klasse MyFrame aber eher ein Nebendarsteller, abgesehen von ihren Standardaufgaben (Exit, Infotext anzeigen, Menuhandling) werden ihr die restlichen Aufgaben von GamePanel abgenommen. Das ist ganz bewusst so gewählt, es bietet einige Vorteile, zum einen kann man später das Aussehen der Applikation leicht verändern, falls man z.B. ein MDI Tic Tac Toe Spiel umsetzen will, oder aber auch die Klasse MyFrame kopieren, und wo anders wieder als Basis für z.b. ein 4 Gewinnt Spiel nutzen.
Die Klasse GamePanel kümmert sich nun um das eigentliche Spielgeschehen, und empfängt auch die Events unseres Steuerelementes FieldPanel:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class GamePanel : public wxPanel
{
    GameLogic gamelogic;// die eigentliche Spiellogik wurde (teilweise) ausgelagert
    std::vector<FieldPanel*> panels;// das Spielfeld
    int freefields;// Hilfsvariable
    wxStaticText* player1;// Damit es netter aussieht
    wxStaticText* player2;// Damit es netter aussieht Vol2
    int player1_won;// Counter zum zählen wer wann gewonnen hat
    int player2_won;
    wxString playername;
protected:
public:
    GamePanel(wxWindow* parent, int id);
    virtual ~GamePanel();
    void OnTurnChange(TurnEvent& event);// Die Eventmethode
    void Clear();
};
// GamePanel Constructor
GamePanel::GamePanel(wxWindow* parent, int id): wxPanel(parent,id)
{
    wxFlexGridSizer* flexgridsizer= new wxFlexGridSizer(1,2,0,0);
    flexgridsizer->AddGrowableRow(0);
    flexgridsizer->AddGrowableCol(0);
    wxGridSizer *gSizer1;
    gSizer1 = new wxGridSizer(3, 3, 0, 0);// Spielfeldsizer
    for(int i =0; i < 9; i++)// füllen des Spielfeldes mit Spielsteinen
    {
        panels.push_back(new FieldPanel(this,i));
        gSizer1->Add(panels.back(), 0, wxEXPAND | wxALL, 5);
    }
    flexgridsizer->Add(gSizer1,0, wxEXPAND | wxALL, 5);
    wxBoxSizer* box= new wxBoxSizer(wxVERTICAL);
    playername = wxT("Player1");
    player1_won =0;
    player2_won=0;
    // Zum Anzeigen wer wie oft gewonnen hat
    player1 = new wxStaticText(this,-1,playername + wxString::Format(" wins %i games",player1_won));
    box->Add(player1,0, wxALL, 5);
    player2 = new wxStaticText(this,-1,wxString::Format("Computer wins %i games",player2_won));
    box->Add(player2,0, wxALL, 5);
    flexgridsizer->Add(box,0, wxALL, 5);
    this->SetSizer(flexgridsizer);
    this->SetAutoLayout(true);
    this->Layout();
    // Der Event für den nächsten Spielzug
    Connect(EVT_TURN_CHANGE,TurnEventHandler(GamePanel::OnTurnChange));
   
    freefields = 9;
}


Ich habe mich entschlossen die eigentliche Spiellogik nicht in diesem Tutorial zu behandeln, da sie im eigentlichen Sinne nichts mit wxWidgets zu tun hat, dennoch ist sie vollständig in der Beispielanwendung implementiert, den Link zum vollständigen Code der Anwendung befindet sich am Ende des Tutorials.

Die Anwendung sieht nun so aus:




4 Ein eigener Dialog


Wie man auf dem obigen Bild und im GamePanel Konstruktor sieht, ist der Standardname für den Spieler "Player1". Als Programmierer kann man den Namen des Spielers nicht wissen, folglich sollte man dem Spieler eine Möglichkeit geben, diesen zu ändern, ebenfalls kann man in diesem Settingsdialog die Farben von X und O ändern:



Die Klasse SettingsDlg ist von wxDialog abgeleitet, und hat neben einem Konstruktor jeweils Handler für die X und O Buttons:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SettingsDlg : public wxDialog
{
    wxColour xcolor;
    wxColour ocolor;
    wxButton* btn_xcolor;
    wxButton* btn_ocolor;
    // Diese Panel dienen der Darstellung der Farbe
    wxPanel* xpanel;
    wxPanel* opanel;
    wxTextCtrl* txt_name;
protected:
public:
    SettingsDlg(wxString playername, wxWindow* parent, int id);
    virtual ~SettingsDlg();
    void OnXColor(wxCommandEvent& event);
    void OnOColor(wxCommandEvent& event);
};


Der Konstruktor:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
SettingsDlg::SettingsDlg(wxString playername, wxWindow* parent, int id):
    wxDialog(parent,wxT("Application Settings"),id), xcolor(FieldPanel::xcolor), ocolor(FieldPanel::ocolor)
{
    wxBoxSizer* mainbox = new wxBoxSizer(wxVERTICAL);
   
    // xcolor Settings
    wxStaticBoxSizer* xcolorbox = new wxStaticBoxSizer(wxHORIZONTAL,this,wxT("X Colorsettings"));
   
    xpanel = new wxPanel(this,-1);
    xpanel->SetBackgroundColour(xcolor);
    xcolorbox->Add(xpanel,0,wxALL|wxEXPAND,5);
    btn_xcolor = new wxButton(this,wxNewId(),wxT("Change Color"));
    xcolorbox->Add(btn_xcolor,0,wxALL,5);
   
    mainbox->Add(xcolorbox,0,wxALL|wxEXPAND,5);
   
    // ocolor Settings
    wxStaticBoxSizer* ocolorbox = new wxStaticBoxSizer(wxHORIZONTAL,this,wxT("O Colorsettings"));
   
    opanel = new wxPanel(this,-1);
    opanel->SetBackgroundColour(ocolor);
    ocolorbox->Add(opanel,0,wxALL|wxEXPAND,5);
    btn_ocolor = new wxButton(this,wxNewId(),wxT("Change Color"));
    ocolorbox->Add(btn_ocolor,0,wxALL,5);
   
    mainbox->Add(ocolorbox,0,wxALL|wxEXPAND,5);
   
    // name Settings
    wxBoxSizer* namebox = new wxBoxSizer(wxHORIZONTAL);
   
    namebox->Add(new wxStaticText(this,-1,wxT("Playername: ")),0,wxALL,5);
    txt_name = new wxTextCtrl(this,wxNewId(),playername);
    namebox->Add(txt_name,0,wxALL,5);
   
    mainbox->Add(namebox,0,wxALL,5);
    // Ok und Cancel Buttons hinzufügen
    mainbox->Add(CreateStdDialogButtonSizer(wxOK|wxCANCEL),0,wxALL,5);
    SetSizer(mainbox);
    SetAutoLayout(true);
    Layout();
    Fit();
   
    // Eventhandler
    Connect(btn_xcolor->GetId(),wxEVT_COMMAND_BUTTON_CLICKED,wxCommandEventHandler(SettingsDlg::OnXColor));
    Connect(btn_ocolor->GetId(),wxEVT_COMMAND_BUTTON_CLICKED,wxCommandEventHandler(SettingsDlg::OnOColor));
}


4.1 wxWidgets Standarddialoge

wxWidgets bietet für verschiedene Aufgaben Standarddialoge an (z.B. Datei- oder Verzeichnisauswahl, Passwortabfragen, Textabfragen etc.), welche dann den jeweiligen Standarddialogen auf der jeweiligen Plattform entsprechen. Als Beispiel für Standarddialoge verwende ich aber nun wxColourDialog, da wir ja dem Benutzer ermöglichen wollen für das X und O eigene Farben zu vergeben:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// direkter Aufruf des Standarddialoges
void SettingsDlg::OnXColor(wxCommandEvent& event)
{
    wxColourData data;
    data.SetChooseFull(true);
    // der ColourDialog bietet 16 "Standardfarben" an.
    for (int i = 0; i < 16; i++)
    {
        wxColour colour(i*16, i*16, i*16);
        data.SetCustomColour(i, colour);
    }
    // Die vorausgewählte Farbe, Standard ist Schwarz
    data.SetColour(xcolor);
    wxColourDialog dialog(this,&data);
    if (dialog.ShowModal() == wxID_OK)
    {
        wxColourData retData = dialog.GetColourData();
        xcolor = retData.GetColour();
        xpanel->SetBackgroundColour(xcolor);
        xpanel->Refresh();
    }
}
// wxWidgets bietet auch vorgefertigte Funktionen, mit denen man die Standarddialoge aufrufen kann:
void SettingsDlg::OnOColor(wxCommandEvent& event)
{
    wxColour col = wxGetColourFromUser(this, ocolor);
    // mit col.Ok() wird der Returnwert des Dialoges überprüft
    if(col.Ok())
    {
        ocolor = col;
        opanel->SetBackgroundColour(ocolor);
        opanel->Refresh();
    }
}


In der Klasse GamePanel wird der Dialog aufgerufen:

C++:
1
2
3
4
5
6
7
8
9
10
11
void GamePanel::ShowSettings()
{
    SettingsDlg dlg(playername,this,-1);
    if(dlg.ShowModal() == wxID_OK)
    {
        FieldPanel::xcolor = dlg.Getxcolor();
        FieldPanel::ocolor = dlg.Getocolor();
        playername = dlg.Getplayername();
        Refresh();
    }
}


5. Dateien speichern und laden

Jetzt fehlt nur noch dass die Anwendung die Einstellungen auch speichern kann. Man kann problemlos unter wxWidgets die STL nutzen, und mittels <fstream> Dateien schreiben und lesen. wxWidgets bietet jedoch auch eigene Streamklassen an, welche auch Support für Zipstreams oder Sockets bieten.
Für die Beispielanwendung jedoch reicht ein simpler wxFileStream aus:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Die Einstellungen in einer Datei speichern
void GamePanel::SaveSettings()
{
    wxFileOutputStream file("settings");
    if(file.Ok())
    {
        wxTextOutputStream out(file);
        out << FieldPanel::xcolor.Red() << ' ' << FieldPanel::xcolor.Green() << ' '<< FieldPanel::xcolor.Blue() << endl;
 
        out << FieldPanel::ocolor.Red() << ' '<< FieldPanel::ocolor.Green() << ' '<< FieldPanel::ocolor.Blue() << endl;
 
        out << playername;
    }
}
 
// Die Einstellungen aus einer Datei laden
void GamePanel::LoadSettings()
{
    wxFileInputStream file("settings");
    if(file.Ok())
    {
        wxTextInputStream in(file);
        int r,g,b;
        in >> r >> g >> b;
        FieldPanel::xcolor.Set(r,g,b);
        in >> r >> g >> b;
        FieldPanel::ocolor.Set(r,g,b);
        playername = in.ReadLine();
    }
}


Den Code der Beispielanwendung findet sich hier.

Weitere Links zum Thema wxWidgest:
http://www.wxwidgets.org
Die offizielle wxWidgets Klassenliste (englisch)
Das offizielle wxWidgets Forum (englisch)

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign