[Home]  [Prev]  [Next]    Guidelines for designing applications with a consistent look and feel

2. Basic frame layout

The first a user sees of an application is its initial frame layout. So even if it might be completely changed afterwards it shouldn't be designed hastily.

A frame layout has some standard elements like titlebar, menu, optional toolbars, optional statusbar, etc.


2.1 Initial layout size and position

How large should the initial layout be? Usually an applications minimal requirement determines the initial size, so i.e. a calendar just uses the size it needs. Often the size is not easily determined since what a user wants to do is not known. I.e. an editor window should be as large as possible but still some room should be left so the essential parts of the underlying desktop are still shown. Also it doesn't make much sense to use a full window on a 21" screen.

A good practice is to set the minimal size to the smallest commonly used screen size (640x480) minus what's needed for other desktop elements like taskbar, home drive, etc. With increasing screen size the minimal size might be extended, e.g. until the current letter format (A4) in portrait mode is reached, always leaving more space for the desktop.

It's not a good idea to start an application with a maximized layout since a user might simply switch to normal display and immediately discovers a "stamp" sized layout. Hence the initial layout should always be shown in normal view.

Sample code

class AppFrame: public wxFrame {... ... wxRect DetermineFrameSize (); ... }; AppFrame::AppFrame (... ... SetSize (DetermineFrameSize ()); ... } wxRect AppFrame::DetermineFrameSize () { wxSize scr = wxGetDisplaySize(); // determine default frame position/size wxRect normal; if (scr.x <= 640) { normal.x = 40 / 2; normal.width = scr.x - 40; }else{ normal.x = (scr.x - 640) / 2; normal.width = 640; } if (scr.y <= 480) { normal.y = 80 / 2; normal.height = scr.y - 80; }else{ normal.y = (scr.y - 400) / 2; normal.height = 400; } return normal; }

2.2 Configuring the layout

Even the most sophisticated initial layout never provides what the users want since they might set entirely different conditions. The layout has to be configurable so it's set the next time to the size the user wishes.

An easy solution is to save the last used layout. But in this case the user can't change the layout temporarily without changing the saved layout as well. A better approach is to save the latest layout on exit only when a certain condition is reached. This can either be a flag in the preferences or still better through a defined exit action. This exit action could be the activated close box with the mouse while pressing the "command" (on Windows CTRL) key. A third (in my opinion just additional) way is through a "save current layout position" command.

Sample code

#include <wx/config.h> // configuration support const wxString APP_NAME = _T("Demo"); const wxString APP_VENDOR = _T("wxGuide"); const wxString LOCATION = _T("Location"); const wxString LOCATION_X = _T("xpos"); const wxString LOCATION_Y = _T("ypos"); const wxString LOCATION_W = _T("width"); const wxString LOCATION_H = _T("height"); class AppFrame: public wxFrame { public: ... void OnFrameLayout (wxCommandEvent &event); private: ... wxRect DetermineFrameSize (); void StoreFrameSize (); ... } bool App::OnInit () { SetAppName (APP_NAME); SetVendorName (APP_VENDOR); ... } BEGIN_EVENT_TABLE (AppFrame, wxFrame) ... EVT_MENU (myID_FRAMELAYOUT, AppFrame::OnFrameLayout) ... END_EVENT_TABLE () AppFrame::AppFrame (... ... SetSize (DetermineFrameSize ()); ... // Window menu wxMenu *menuWindow = new wxMenu; ... menuWindow->AppendSeparator(); menuWindow->Append (myID_FRAMELAYOUT, _("Store window size")); ... } // this works currently only under Windows void AppFrame::OnClose (wxCloseEvent &event) { ... if (myIsKeyDown (WXK_CONTROL) && !myIsKeyDown ('Q')) { StoreFrameSize (GetRect ()); } ... } void AppFrame::OnFrameLayout (wxCommandEvent &WXUNUSED(event)) { StoreFrameSize (GetRect ()); wxMessageBox (_("The position and size of the window are stored."), _("Store window size"), wxOK); } wxRect AppFrame::DetermineFrameSize () { const int minFrameWidth = 80; const int minFrameHight = 80; ... // load stored size or defaults wxRect rect; wxConfig* cfg = new wxConfig (APP_NAME); int i; for (i = 0; i <= m_frameNr; i++) { wxString key = LOCATION + wxString::Format ("%d", m_frameNr - i); if (cfg->Exists (key)) { rect.x = cfg->Read (key + _T("/") + LOCATION_X, rect.x); rect.y = cfg->Read (key + _T("/") + LOCATION_Y, rect.y); rect.width = cfg->Read (key + _T("/") + LOCATION_W, rect.width); rect.height = cfg->Read (key + _T("/") + LOCATION_H, rect.height); break; } } delete cfg; // check for reasonable values (within screen) rect.x = wxMin (abs (rect.x), (scr.x - minFrameWidth)); rect.y = wxMin (abs (rect.y), (scr.y - minFrameHight)); rect.width = wxMax (abs (rect.width), (minFrameWidth)); rect.width = wxMin (abs (rect.width), (scr.x - rect.x)); rect.height = wxMax (abs (rect.height), (minFrameHight)); rect.height = wxMin (abs (rect.height), (scr.y - rect.y)); return rect; } void AppFrame::StoreFrameSize (wxRect rect) { // store size wxConfig* cfg = new wxConfig (APP_NAME); wxString key = LOCATION + wxString::Format ("%d", m_frameNr); cfg->Write (key + _T("/") + LOCATION_X, rect.x); cfg->Write (key + _T("/") + LOCATION_Y, rect.y); cfg->Write (key + _T("/") + LOCATION_W, rect.width); cfg->Write (key + _T("/") + LOCATION_H, rect.height); delete cfg; } inline bool myIsKeyDown (int nVirtKey) { // see file "private.h" }

2.3 Different layouts

If an application has several different layouts, i.e. a database information tool with reporting and a login layout, it should always show the larges first and any other on top of the first. Of course this also means that any layout after the first has to have an equal or smaller size. I.e. a preference dialog should never be larger that the working layout.


2.4 Multiple equal layouts

An application may handle multiple equal layouts, mostly for multiple documents. These may be shown either as a notebook within a single window or as completely separate windows. If separate windows were chosen they should be designed in a way that switching between them with the task manager (ALT-TAB), or similar concept, is possible.

Sample code

class App: public wxApp { ... //! frame window WX_DEFINE_ARRAY (AppFrame*, AppFrames); AppFrames m_frames; void CreateFrame (wxArrayString *fnames); void RemoveFrame (AppFrame *frame); ... }; bool App::OnInit () { ... // create application frame int nr = m_frames.GetCount(); m_frames.Add (new AppFrame (g_appname, *m_fnames, nr)); // open application frame m_frames[0]->Layout (); m_frames[0]->Show (true); SetTopWindow (m_frames[0]); ... }; void App::CreateFrame (wxArrayString *fnames) { int nr = m_frames.GetCount(); m_frames.Add (new AppFrame (g_appname, *fnames, nr)); m_frames[nr]->Layout (); m_frames[nr]->Show (true); SetTopWindow (m_frames[nr]); } void App::RemoveFrame (AppFrame *frame) { m_frames.Remove (frame); }

Remark: RemoveFrame is called within AppFrame::OnClose which isn't shown, for the full code look at the Demo sample.

If a application handles multiple documents it probably also takes care that no document is opened twice. This is only possible if a single instance handles all documents. A solution for this is the SingleInstanceChecker, with this a second instance can be discovered and the intended work can be sent to the first instance.

Sample code

#include <wx/ipc.h> // IPC support const wxString IPC_START = _T("StartOther"); static wxString g_appname; ... class App: public wxApp { friend class AppIPCConnection; ... //! object used to check if another program instance is running wxSingleInstanceChecker *m_singleInstance; //! the wxIPC server wxServerBase *m_serverIPC; //! frame window AppFrame *m_frame; bool ProcessRemote (wxChar** argv, int argc = 0); }; class AppIPCConnection : public wxConnection { public: //! application IPC connection AppIPCConnection(): wxConnection (m_buffer, WXSIZEOF(m_buffer)) { } //! execute handler virtual bool OnExecute (const wxString& WXUNUSED(topic), wxChar *data, int size, wxIPCFormat WXUNUSED(format)) { wxChar** argv; int argc = 0; for (int i=0; i<size; i++) { if ((i > 0) && (data[i] == '\0') && (data[i-1] == '\0')) break; if (data[i] == '\0') argc++; } argv = new char*[argc]; int p = 0; wxChar* temp = data; for (int j=0; j<argc; j++) { argv[j] = new char [wxStrlen (temp) + 1]; wxStrcpy (argv[j], temp); p = wxStrlen (temp) + 1; temp += p; } bool ok = wxGetApp().ProcessRemote (argv, argc); for (int k=0; k<argc; k++) { delete [] argv[k]; } delete [] argv; return ok; } private: // character buffer wxChar m_buffer[4096]; }; class AppIPCServer : public wxServer { public: //! accept conncetion handler virtual wxConnectionBase *OnAcceptConnection (const wxString& topic) { if (topic != IPC_START) return NULL; return new AppIPCConnection; } }; bool App::OnInit () { ... g_appname.Append (APP_VENDOR); g_appname.Append (_T("-")); g_appname.Append (APP_NAME); ... // Set and check for single instance running wxString name = g_appname + wxString::Format (_T("%s"), wxGetUserId().c_str()); m_singleInstance = NULL; m_singleInstance = new wxSingleInstanceChecker (name); if (m_singleInstance->IsAnotherRunning()) { wxClient client; wxConnectionBase *conn = client.MakeConnection (wxEmptyString, name + _T(".ipc"), IPC_START); if (conn) { wxString dataStr = wxEmptyString; for (int i = 0; i < argc; ++i) { dataStr.Append (argv[i]); dataStr.Append (_T(" ")); } wxChar data[4096]; wxStrcpy (data, dataStr.c_str()); int size = 0; for (i = 0; i < argc; ++i) { size += strlen (argv[i]); data[size] = '\0'; size += 1; } data[size] = '\0'; size += 1; if (conn->Execute (data, size)) return false; } delete conn; } // IPC server m_serverIPC = NULL; m_serverIPC = new AppIPCServer (); if (!m_serverIPC->Create (name + _T(".ipc"))) { delete m_serverIPC; m_serverIPC = NULL; } ... }

2.5 Titlebar

The titlebar of a top level window usually shows the name of the application and the essential information about what this window contains. It should the user allow to easily distinguish between windows on his desktop. Special care has to be taken if an application has multiple equal or even different top level windows. It's important that also in this case the user is able to distinguish between them.

Most of the time the essential information is the name of the file being worked on. Sometimes other information like "readonly" are also show. Unless such information is necessary for the distinction of top level windows they should be shown somewhere else. I.e. versioning information should be moved into the "About .." box.

Titlebar decorations, like close box, etc. are not part of an application. An application simply has to act on any event produced. In special cases (only then) an application may decide to disable any of these decoration.

Child windows should just show its task and if need the item acted upon, never the applications name, since child windows may only be showed together with the top level window. If this isn't wished a child window should be changed into a top level window itself.

Sample code

//! global application name wxString g_appname; bool App::OnInit () { // set application and vendor name SetAppName (APP_NAME); SetVendorName (APP_VENDOR); g_appname.Append (APP_NAME); ... } void AppFrame::UpdateTitle () { wxString title = g_appname; if (title != GetTitle()) SetTitle (title); }

2.6 Toolbars

Toolbars usually allows to access the most important commands through icons to give novice users an overview what they can do, but think even novice may become accustomed to your application. Consider toolbar uses precious screen area which could be used otherwise. Always allow to hide toolbar for user who don't like/need them and put only items into toolbars which are better accessable through them.

Toolbars are very usefil to contain the navigation selection between items through a combobox. If a large set of items are used the selection via menu is rather awkward and much easier with a combobox. Also a combobox doesn't require an otherwise need permanent pane for navigation between items.

To make the implementation of navigation selection easier a filelist class was created. It handles the update of the combobox and optional a corresponding menu. Only the use of this class is shown here. The class itself can be gotten via here.

Sample code

Important notice: wxWidgets has a fatal bug when a combo box changes its content dynamically during an event. Currently better not use wxComboBox as suggested below.

#include <wx/toolbar.h> // toolbars support #include "filelist.h" // FileList control class AppFrame: public wxFrame { ... void OnComboboxChange (wxCommandEvent &event); ... //! toolbar void CreateToolbar (); wxToolBar *m_toolbar; void DeleteToolbar (); wxComboBox *m_pageSelect; //! open file list wxFileList *m_filelist; wxMenu *m_filesMenu; ... } BEGIN_EVENT_TABLE (AppFrame, wxFrame) ... EVT_COMBOBOX (-1, AppFrame::OnComboboxChange) ... END_EVENT_TABLE () AppFrame::AppFrame (... ... // initialize toolbars m_filelist = new wxFileList (-1, myFLIST_STARTSEPARATOR | myFLIST_CTRL_KEYS); m_toolbar = NULL; m_pageSelect = NULL; if (!m_toolbar && g_CommonPrefs.showToolbar) { CreateToolbar (); } ... m_filelist->Add (...); ... } AppFrame::~AppFrame () { delete m_filelist; ... } void AppFrame::OnClose (wxCloseEvent &event) { ... if (m_pageSelect) m_filelist->RemoveCbox (m_pageSelect); ... } void AppFrame::OnFileClose (wxCommandEvent &WXUNUSED(event)) { ... m_filelist->Remove (m_demo->GetLabel()); ... } void AppFrame::OnComboboxChange (wxCommandEvent &WXUNUSED(event)) { if (!m_pageSelect) return; m_pageNr = GetPageNr (m_filelist->Item (m_pageSelect->GetSelection())); m_book->SetSelection (m_pageNr); PageHasChanged (m_pageNr); } void AppFrame::CreateToolbar () { if (m_toolbar) return; // create toolbar m_toolbar = wxFrame::CreateToolBar (wxTB_TEXT|wxTB_FLAT); // create session selector box m_pageSelect = new wxComboBox (m_toolbar, myID_PAGESELECT, wxEmptyString, wxDefaultPosition, wxSize (32*GetCharWidth(),-1), 0, NULL, wxCB_READONLY); m_toolbar->AddControl (m_pageSelect); m_filelist->UseCbox (m_pageSelect, 32); m_toolbar->Realize(); } void AppFrame::DeleteToolbar () { if (!m_toolbar) return; // remove session selector box m_filelist->RemoveCbox (m_pageSelect); // delete toolbar SetToolBar (NULL); delete m_toolbar; m_toolbar = NULL; m_pageSelect = NULL; } void AppFrame::PageHasChanged (int pageNr) { // see "app.cpp" of the demo application }

2.7 Statusbar

The statusbar shows all the immediately needed and active information. It usually also shows the last action done. But it's not a good idea to show error messages only in the statusbar.

Sample code

wxString g_statustext; AppFrame::AppFrame (... ... // initialize statusbar static const int widths[] = {-1}; CreateStatusBar (WXSIZEOF(widths), wxST_SIZEGRIP, myID_STATUSBAR); SetStatusWidths (WXSIZEOF(widths), widths); // update information g_statustext = _("Welcome to the Demo application"); ... } void AppFrame::UpdateStatustext () { SetStatusText (g_statustext, 0); }

Note: Updating the statusbar on wxGTK (version 2.4) uses up to 100% CPU, to reduce this see in the Demo sample how to implement the m_updateTimer timer.