Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Mapping the User Interface

0.00/5 (No votes)
3 Nov 2003 1  
Designing a user interface for non-rectangular hotspots or a window with an excessive number of controls.

Table of Contents

Introduction

A Mapped User Interface (or MUI), is a user interface with items on the window defined by a simple graphical "map". The map is a bitmap mask of demarcation with unique colors assigned to each user interface item, everything else is black. For an example map, look at the screen shots provided. The color of a point on the map is retrieved and used to look up an associated object, which uses an RGB color as a unique identifier.

The code is simple and requires less work to define the map than other methods, like defining regions or rectangles for each UI element. It does require a database of some kind, either a flat file, XML, or hard-coded array, to look up the object data by unique color. In the example application, comma delimited files were used.

"So under what circumstances would I want to use this method?", you ask. If you have a large graphic that you want to use as a basis for a user interface, such as a world map, or a picture of a piece of machinery, and you want the user to be able to click on different areas to get more information, then MUIs are for you. That brings us to one specific problem a CP user brought up which was the impetus for this idea.

The Problem

Back on September 22 (2003), b_girl posed this question [^] on the Visual C++ discussion board:

Part of the interface that I'm developing requires that there be some sort of interactive periodic table of the elements. I'm not completely sure how I should go about doing this and I was wondering if anyone here had any good ideas for me, or knew where I could find some info on it. I need it to look like the traditional periodic table, but the user is able to click on any number of the elements, and the element number for the ones they select will end up in a list. The only thing I could think of doing was creating one button for each of the elements (which is a lot of buttons!!) and each time a button is clicked, the element number is either added or removed from the list. Anyone have any better ideas they'd like to share with me?

A couple of good ideas were given, but in my awkward way of thinking, I came up with the Mapped User Interface (MUI) concept [^]. At the time, I thought it was a unique idea, only to find out later that I had been pre-empted by Paul DiLascia 6 years prior. More about that later.

Multiple Solutions

Like I said, several good ideas were given, including Terry O'Nolley [^]'s GDI solution [^], of which he also wrote a good article. Other possible solutions to this particular problem include using a grid control, and using a large bitmap with elements bounded by rectangles. These are all good solutions for a periodic table, but I think MUIs are easy to implement and their pros outweigh the cons.

Pros and Cons

MUI
Pros Cons
Less code than other methods Requires graphics ability
Easy maintenance (just update the map bitmap and the database) Messy text if aspect ratio not maintained
Can resize using StretchBlt function  
Non-rectangular UI elements can be defined  

Mapping the User Interface

If you have a good candidate application for using a MUI, the first thing you need to do, is find a good graphic to use as your main display picture. Once you have found that perfect picture, you will need to make the map bitmap. I used Paint Shop Pro (MS Paint would work just as well) to "cut-out" the shape of each UI item. First, make a duplicate copy of your picture and open it in PSP. Then choose a unique color for an object and make it your background color, then take the lasso tool and outline around the UI item. When the outline is finished, hit the delete button and the item will be filled with one color. Repeat this for each UI item you want to handle. When all items have been given a unique color, you need to make the rest of the picture black. In PSP, hold down the shift key and click each of your colored items, this should select each one, then go to the Selections menu and choose Invert. This will select everything except your UI items. Now change your background color to black (RGB(0,0,0,)) and hit Delete. Everything should be black except your UI items. Your map graphic is now finished.

That was easy, now comes the most tedious part, we need to find the RGB value of each UI item and add it to a color value in a database or flat file. I will leave this step up to you, but I did it using Excel and a simple application that used GetPixel to extract the RGB value from the map. See the CSV files in the demo source zip file for an example. FYI: Excel is great for stuff like this.

Now that you have your map and your database, let's look at some of the code in more detail.

Essential Code

I have defined some base classes to help make using MUIs easier. CMUILocation is the base class the UI item objects inherit from. Each map "location" will always have a color (the unique identifier) and a name. The descendant classes define the secondary properties for each object type.

class CMUILocation
{
public:
    CMUILocation(void) 
    {
        crColor = RGB(0,0,0);
        cstrName = "";
    };

    virtual ~CMUILocation(void) {};

    COLORREF GetColor() { return crColor; };
    void SetColor(COLORREF color) { crColor = color; };

    CString GetName() { return cstrName; };
    void SetName(CString name) { cstrName = name; };

protected:
    COLORREF crColor;
    CString cstrName;
};

CMappedUserInterface is an abstract base class that handles drawing and also retrieval of "locations" by color. A CArray is used to store CMUILocation(s) for quick retrieval. The LoadDataFromFile function loads the locations into the locationList CArray. It is implemented in the descendant class because location objects can have different properties. For example, the Element object has color, name, atomic mass and atomic number properties, while the UnitedStates object in the demo application has color, name, abbreviation, population, and state bird properties.

class CMappedUserInterface
{
public:
    CMappedUserInterface(UINT nVisBmpID, UINT nTemplateBmpID, 
        CWnd* pDraw);

    //... some code not shown


    CMUILocation* GetLocationFromColor(COLORREF crColor);
    COLORREF GetTemplateBMPColorAtPoint(CPoint pt);

    //... some code not shown


    void DrawMUI(CDC* pDC);
protected:
    CArray<CMUILOCATION*, CMUILocation*> locationList;
    CBitmap VisibleBMP;
    CBitmap TemplateBMP;

    //... some code not shown


    virtual BOOL LoadDataFromFile(CString cstrDataFile) = 0;
};
void CMappedUserInterface::DrawMUI(CDC* pDC)
{
    //... variable initialization code not shown

    pDrawWnd->GetClientRect(&rect);

    // create compatible DCs

    templateDC.CreateCompatibleDC(pDC);
    visibleDC.CreateCompatibleDC(pDC);
    srcDC.CreateCompatibleDC(pDC);

    // create a sized bitmap for the memory DCs

    newBmp.CreateCompatibleBitmap(pDC, rect.Width(), 
        rect.Height());
    
    // stretch the visible bitmap onto the DC

    pOldBmp = visibleDC.SelectObject(&newBmp);
    pOldSrcBmp = srcDC.SelectObject(&VisibleBMP);

    iOldStretchBltMode = visibleDC.SetStretchBltMode(HALFTONE);

    visibleDC.StretchBlt(0, 0, rect.Width(), rect.Height(), 
        &srcDC, 0, 0, bmpVisible.bmWidth, bmpVisible.bmHeight, 
        SRCCOPY);

    visibleDC.SetStretchBltMode(iOldStretchBltMode);

    // now blit it to the screen DC

    pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &visibleDC, 
        0, 0, SRCCOPY);

    srcDC.SelectObject(pOldSrcBmp);
    visibleDC.SelectObject(pOldBmp);

    //... cleanup code not shown

}

CMUILocation* 
CMappedUserInterface::GetLocationFromColor(COLORREF crColor)
{
    CMUILocation* pLocation = NULL;

    // loop through each location in our list and 

    //    find the matching color

    for (int i = 0; i < locationList.GetSize(); i++)
    {
        pLocation = (CMUILocation*) locationList.GetAt(i);
        if (pLocation->GetColor() == crColor)
            break;
        else
            pLocation = NULL;
    }

    return pLocation;
}

COLORREF 
CMappedUserInterface::GetTemplateBMPColorAtPoint(CPoint pt)
{
    //... variable initialization code not shown

    pDrawWnd->GetClientRect(&rect);
    
    pDC = pDrawWnd->GetDC();
    memDC.CreateCompatibleDC(pDC);
    srcDC.CreateCompatibleDC(pDC);

    // create a sized bitmap for the memory DC    

    newBmp.CreateCompatibleBitmap(pDC, rect.Width(), 
        rect.Height());

    pOldBmp = memDC.SelectObject(&newBmp);
    pOldSrcBmp = srcDC.SelectObject(&TemplateBMP);

    // stretch the template bitmap onto a compatible DC

    iOldStretchBltMode = memDC.SetStretchBltMode(HALFTONE);

    memDC.StretchBlt(0, 0, rect.Width(), rect.Height(), 
        &srcDC, 0, 0, bmpTmpl.bmWidth, bmpTmpl.bmHeight, 
        SRCCOPY);

    memDC.SetStretchBltMode(iOldStretchBltMode);

    // get the color on the template at given coordinates

    crColorAtPoint = GetPixel(memDC.m_hDC, pt.x, pt.y);

    srcDC.SelectObject(pOldSrcBmp);
    memDC.SelectObject(pOldBmp);

    //... clean up code not shown


    return crColorAtPoint;
}

In the ChildView of the demo application, the SetMUI function is used to flip between the three different MUIs available. pMui is a pointer to a CMappedUserInterface object, but since that class is abstract, you can't instantiate an object of that type, you must instantiate a descendant and cast it to the ancestor. This works great for the demo app, since I'd like to use one CMappedUserInterface object for each of the different MUIs available. I don't have to define a pointer for each different MUI.

void CChildView::SetMUI(UINT uintMUI)
{
    CString cstrMainFrameText;

    // delete the old MUI pointer so we can make a new one

    if (pMui != NULL) 
    {
        delete pMui;
        pMui = NULL;
    }

    // create a new MUI instance based on the selected type

    switch (uintMUI)
    {
    case USA_MUI:
        pMui = new CUSAMapMUI(IDB_USMAP, 
            IDB_USMAP_TEMPLATE, this);    
        cstrMainFrameText = "Map of the United States "
            "of America";
        break;
    case MARS_MUI:
        pMui = new CMarsMapMUI(IDB_NGS, IDB_NGS_TEMPLATE, 
            this);
        cstrMainFrameText = "Map of the planet Mars";
        break;
    case PTE_MUI:
    default:
        pMui = new CPeriodicTableMUI(IDB_PERIODICTABLE, 
            IDB_PERIODICTABLE_TEMPLATE, this);
        cstrMainFrameText = "The Periodic Table of Elements";
        break;
    }

    uintCurrentMUI = uintMUI;

    if (IsWindow(AfxGetMainWnd()->m_hWnd))
        AfxGetMainWnd()->SetWindowText(cstrMainFrameText);

    // repaint the screen to see the new MUI

    if (IsWindow(m_hWnd)) Invalidate();
}

Once the MUI is set (in the demo it is set in the OnCreate message handler), we can call the DrawMUI member function in our OnPaint handler. The MUI will draw itself, so this all that is needed for drawing at this point.

void CChildView::OnPaint()
{
    CPaintDC dc(this);

    if (pMui != NULL)
        pMui->DrawMUI(&dc);
}

OnLButtonDown shows how to retrieve a CMUILocation object from a CPoint.

void CChildView::OnLButtonDown(UINT nFlags, CPoint point)
{
    COLORREF crColorAtPoint = 0;

    // find the color at this point on the template bitmap

    crColorAtPoint = pMui->GetTemplateBMPColorAtPoint(point);
    
    // get the CMUILocation object based on color at this 

    //    point on the template

    CMUILocation* pLocation = 
        pMui->GetLocationFromColor(crColorAtPoint);

    if (pLocation != NULL)
    {
        // do desired left-click functionality here

    }
}

Applications of MUIs

MUIs aren't good for all applications or windows, only those with non-rectangular hot-spots, or those with an unmanageably large number of controls. Some potential uses include: digital world or country maps, illustrations of complex machinery, or even diagrams of the human body. How many more can you come up with?

Credits

As I mentioned earlier, I wasn't the first person to come up with this idea. Paul DiLascia [^], of MSJ [^] and MSDN [^] magazine fame, wrote about it first back in the March 1997 issue of MSJ. [^]. I only stumbled on this article while looking up how to use tooltips on hot-spots outside of controls. I guess great minds think alike, right? Anyway, his is a good article and I don't want to take any credit away from him. The code for the tooltips was derived from his article, but the rest of it is my own.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here