[Home]  [Prev]  [Next]    A guide, a tutorial for developing well-designed cross-platform applications

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. Just think if the first impression doesn't fit the user's expectations he may simply discard your application without delving deeper for all the terrific features inside. Nothing fancy is needed for the first appearance, just some reasonable defaults. It's not possible to make it well for everybody but with very small work you can find a solution which is good for most.

A frame layout usually has some standard elements like title bar, menu, optional toolbars, optional status bar, 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) is reached, always leaving some space for the desktop. Of course if your applications don't need more space that lets say 200x100 pixels, just design it that way.

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 wrong designed layout like e.g. a "stamp" sized layout. Maximized layout just leaves the impression the developer didn't thought about which immediately pops up the question what else the developer hasn't thought about. 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 always save the last used layout. But in this case the user can't change the layout temporarily without changing the later 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. Unfortunately this isn't possible on all platforms so another 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("wyoGuide");

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, _("Save current layout"));

    ...
}

// 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 should have an equal or smaller size. I.e. a preference dialog should not be larger that the working layout. Again there might exist cases which this isn't appropriate.


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<(int)(size/sizeof(wxChar)); 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.ipc"), wxGetUserId().c_str());
    m_singleInstance = new wxSingleInstanceChecker (name);
    if (m_singleInstance->IsAnotherRunning()) {
        wxClient client;
        wxConnectionBase *conn = client.MakeConnection (wxEmptyString, name, IPC_START);
        if (conn) {
            wxString dataStr;
            for (int i = 0; i < argc; ++i) {
#ifndef __linux__
                dataStr.Append (argv[i]);
#else // on Linux fixes relative to absolute path
                wxFileName w(argv[i]);
                w.Normalize (wxPATH_NORM_ALL & ~wxPATH_NORM_CASE);
                dataStr.Append (w.GetFullPath());
#endif
                dataStr.Append (_T(" "));
            }
            wxChar data[4096];
            wxStrcpy (data, dataStr.c_str());
            int size = 0;
            for (i = 0; i < argc; ++i) {
#ifndef __linux__
                size += wxStrlen (argv[i]);
#else // on Linux fixes relative to absolute path
                wxFileName w(argv[i]);
                w.Normalize (wxPATH_NORM_ALL & ~wxPATH_NORM_CASE);
                size += wxStrlen (w.GetFullPath());
#endif
                data[size] = '\0';
                size += 1;
            }
            data[size] = '\0';
            size += 1;
            if (conn->Execute (data, size*sizeof(wxChar))) {
                delete m_singleInstance;
                m_singleInstance = NULL;
                return false;
            }
        }
        delete conn;
    }

    // IPC server
    m_serverIPC = new AppIPCServer ();
    if (!m_serverIPC->Create (name)) {
      delete m_serverIPC;
      m_serverIPC = NULL;
   }

   ...
}


2.5 Title bar

The title bar of a top level window usually shows the name of the application and the essential information about what object this window contains. In other words it shows the tool name and the object name. 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" is 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, others can be shown in the status bar.

Title bar decorations, like close box, etc. are not part of an application. An application simply has to act on any such event received. In special cases (only then) an application may decide to disable any decorations and veto these events.

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 accessing 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 accessible through them.

Toolbars are very useful 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

#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 Status bar

The status bar 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 status bar.

Sample code

wxString g_statustext;

AppFrame::AppFrame (...
    ...
    // initialize status bar
    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);
}