[X] wxWidgets Tutorial Part II: Spiel mit mir
-
Okay, das passt so schon
-
Gut, habs noch mal durchgelesen, denke das passt so
Setz den Artikel jetzt mal auf R, damit noch genügend Zeit für Korrekturen ist
-
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.
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::clown: { 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 hat, und dann ein neuer Wert gesetzt werden: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 diesem 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 ein Event an sein Parentfenster senden könnte.2.3 Ein eigener Notifyevent
Die Implementierung unseres eigenen Events sieht so aus:
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:// 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, dass 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, dass 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:
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:
// 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, Menühandling) 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: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:
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:
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:
// 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:
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:// 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 finden Sie hier.
Weitere Links zum Thema wxWidgets:
http://www.wxwidgets.org
Die offizielle wxWidgets-Klassenliste (englisch)
Das offizielle wxWidgets Forum (englisch)
-
so, hab die Änderungen gerade eingepflegt.
-
Häng die Version doch bitte hinten an, dann muss ich nicht suchen.
-
Danke!
Kann der Artikel auf E?
-
Ja
-
Eben beim Verschieben sind mir die unterschiedlichen Titel deiner Serie aufgefallen.
Ich habe den Artikel trotzdem veröffentlicht, aber ich möchte den Titel so schnell wie möglich angleichen.Welche Version soll es sein?
-
Ähm, ich hatte ja schon mal darauf hingewiesen, das ich für den Titel Vorschläge suche,
GPC meinte ja das dieser OK sei. Vielleicht ist "Grundlagen von wxWidgets" angebrachter.
-
Damit hätten wir die dritte Möglichkeit.
Ich fände Version 2 gut, weil man den Artikel dann findet, wenn man nach Tutorial sucht.
-
Hm, stimmt. Aber irgendwie reden wir jetzt aneinander vorbei...
Willst du den Titel vom ersten Artikel ändern?
z.b. in "wxWidgets Tutorial Part I: Einführung in wxWidgets" ?
-
Von welchem Artikel ich den Titel andere ist mir egal. Es können auch beide sein.
Ich möchte sie nur einheitlich haben, weil es ja eine Serie sein soll.Aber dein Vorschlag ist gut.
-
Dann machen wir es so
Ich änder den Titel des 1. Artikels wie vorgeschlagen
-
Gut, dann ändere ich alle Stellen wo der so vorkommt.
-
Dieser Thread wurde von Moderator/in estartu aus dem Forum Die Redaktion in das Forum Archiv verschoben.
Im Zweifelsfall bitte auch folgende Hinweise beachten:
C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?Dieses Posting wurde automatisch erzeugt.