I've written an earlier article about installing the Wt C++ Web Toolkit. I think it took longer to get all the bits and pieces properly arranged than it did me to cook up an example to check that it did what I thought it could do.
Saturday, October 6. 2007
Web 2.0 Site Development with Wt -- Dead Easy
I'm quote pleased and impressed with the tool. I was able to get a basic web application up and running in about two days, which were spent reading the documentation, reviewing the class libraries, looking at some of the samples, poking through the PostgreSQL C library, and then hacking stuff into the basic Wt 'Hello World' application.
The authors of Wt have done a great job of hiding all the Javascript go-between code. I can focus on the higher level parts of my design, rather than the underlying machinery to make an interface work. About the only thing I've encountered that I'd like to change is to take the inline Javascript code, and get it put into a .js file.
The test I came up with was to open up a database, select a bunch of records, and present the records in a table which could be stepped through (forwards and backwards) 10 records at a time... and all this without having to refresh the whole page, just the table itself. The following code proves it can be done.
It allows one to turn an internet browser into a true application front end with most of the usual gui functions provided.
This first file is ouipam.cpp, the file with the 'main' start for each session connection:
//============================================================================ // Name : ouipam.cpp // Author : Ray Burkholder // Version : // Copyright : (c) 2007 One Unified // Description : OUIPAM: One Unified IP Address Management //============================================================================ // server push uses WApplication enableUpdates(), triggerUpdate(), getUpdateLock(); #include "Responder.h" using namespace Wt; WApplication *createApplication(const WEnvironment& env) { // Instantiate the Wt application. Responder *appl = new Responder(env); return appl; } int main(int argc, char **argv) { return WRun(argc, argv, &createApplication); }
This file is Responder.h, the header file for the responder, which handles the WApplication (or session):
//============================================================================ // Name : ouipam.cpp // Author : Ray Burkholder // Version : // Copyright : (c) 2007 One Unified // Description : OUIPAM: One Unified IP Address Management //============================================================================ #ifndef RESPONDER_H_ #define RESPONDER_H_ #include <WApplication> #include <WContainerWidget> #include <WText> #include <WPushButton> #include <WTable> #include <WTableCell> #include "libpq-fe.h" using namespace Wt; class Responder : public WApplication { public: Responder(const WEnvironment& env); virtual ~Responder(); protected: WPushButton *button; WTable *table; private: int cnt; PGconn *conn1; PGresult *result; int nRowsToShow; int cntRowsFound; int cntColumnsFound; int ixFirstRowShowing; // 0 offset void OnButtonListBegin(); void OnButtonForeward(); void OnButtonBackward(); void OnButtonListEnd(); void ShowRows(); }; #endif /*RESPONDER_H_*/
This final file is Responder.cpp, where all the session work is performed. The constructor opens the database, creates the webpage basics, and assigns events to each of the forward and backward buttons. When a button is pressed, the appropriate method is called to use the appropriate records and update the table. Again, with Javascript enabled, the web page is not refreshed. Instead, the browser's DOM is updated directly, and only the table cells change. I look foreward to seeing what else I can do with this flexibility. As you can see from the header comment, this is the start of an interactive web application for managing an organizations IP Addresses.
//============================================================================ // Name : ouipam.cpp // Author : Ray Burkholder // Version : // Copyright : (c) 2007 One Unified // Description : OUIPAM: One Unified IP Address Management //============================================================================ #include "Responder.h" #include "WBreak" #include <sstream> #include <ostream> #include <algorithm> using namespace std; Responder::Responder(const WEnvironment& env) : WApplication(env ) { // Set application title setTitle("OUIPAM by One Unified"); ostringstream ss; // perform database query conn1 = PQconnectdb("hostaddr=127.0.0.1 port=5432 dbname=oneunified user=oneunified password=xxx"); ConnStatusType stat = PQstatus(conn1 ); if (CONNECTION_OK != stat) { //PQfinish(conn1); ss << "pq result= bad("<< stat << ")"<< endl; root()->addWidget(new WText(ss.str())); } else { result = PQexec(conn1, "select * from ianaiftype;"); ExecStatusType statusExec = PQresultStatus(result ); ss << "pgresult="; bool bTuplesFound = false; switch (statusExec ) { case PGRES_EMPTY_QUERY: ss << "empty query"; break; case PGRES_COMMAND_OK: ss << "command ok"; break; case PGRES_TUPLES_OK: ss << "tuples found"; bTuplesFound = true; break; case PGRES_COPY_OUT: ss << "copy out"; break; case PGRES_COPY_IN: ss << "copy in"; break; case PGRES_BAD_RESPONSE: ss << "bad response"; break; case PGRES_NONFATAL_ERROR: ss << "non fatal error"; break; case PGRES_FATAL_ERROR: ss << "fatal error"; break; } root()->addWidget(new WText(ss.str())); root()->addWidget(new WBreak()); // present some query statistics if (bTuplesFound ) { cntRowsFound = PQntuples(result ); cntColumnsFound = PQnfields(result ); ss.str(""); ss << "rows="<< cntRowsFound << ", columns="<< cntColumnsFound << endl; root()->addWidget(new WText(ss.str())); root()->addWidget(new WBreak()); for (int i = 0; i < cntColumnsFound; i++) { ss.str(""); ss << PQfname(result, i )<< ", format="<< PQfformat(result, i ) << ", type="<< PQftype(result, i )<< ", size=" << PQfsize(result, i )<< endl; root()->addWidget(new WText(ss.str())); root()->addWidget(new WBreak()); } // create table for row results table = new WTable(); root()->addWidget(table); // assign methods to the buttons button = new WPushButton( L"<<" ); root()->addWidget(button ); button->clicked.connect(SLOT(this, Responder::OnButtonListBegin)); button = new WPushButton( L"<" ); root()->addWidget(button ); button->clicked.connect(SLOT(this, Responder::OnButtonBackward)); button = new WPushButton( L">" ); root()->addWidget(button ); button->clicked.connect(SLOT(this, Responder::OnButtonForeward)); button = new WPushButton( L">>" ); root()->addWidget(button ); button->clicked.connect(SLOT(this, Responder::OnButtonListEnd)); // show the query results nRowsToShow = 10; ixFirstRowShowing = 0; ShowRows(); } } } Responder::~Responder() { PQclear(result ); // result exists even with new command, and even if connection is closed; PQfinish(conn1 ); } void Responder::ShowRows() { // update rows in the already created table WTableCell *cell; WText *text; int ixRowToShow = ixFirstRowShowing; for (int ixTableRow = 0; ixTableRow < nRowsToShow; ixTableRow++) { for (int ixColumn = 0; ixColumn < cntColumnsFound; ixColumn++) { cell = table->elementAt(ixTableRow, ixColumn ); cell->clear(); if (ixRowToShow < cntRowsFound ) { text = new WText(); text->setFormatting(WText::PlainFormatting ); text->setText(PQgetvalue(result, ixRowToShow, ixColumn ) ); cell->addWidget(text ); } else { } } ixRowToShow++; } } void Responder::OnButtonListBegin() { ixFirstRowShowing = 0; ShowRows(); } void Responder::OnButtonBackward() { ixFirstRowShowing = max( 0, ixFirstRowShowing - nRowsToShow ); ShowRows(); } void Responder::OnButtonForeward() { int i = max( 0, cntRowsFound - nRowsToShow ); ixFirstRowShowing = min(i, ixFirstRowShowing + nRowsToShow ); ShowRows(); } void Responder::OnButtonListEnd() { ixFirstRowShowing = max( 0, cntRowsFound - nRowsToShow ); ShowRows(); }