Table of Contents
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.
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.
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.
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 |
|
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.
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);
CMUILocation* GetLocationFromColor(COLORREF crColor);
COLORREF GetTemplateBMPColorAtPoint(CPoint pt);
void DrawMUI(CDC* pDC);
protected:
CArray<CMUILOCATION*, CMUILocation*> locationList;
CBitmap VisibleBMP;
CBitmap TemplateBMP;
virtual BOOL LoadDataFromFile(CString cstrDataFile) = 0;
};
void CMappedUserInterface::DrawMUI(CDC* pDC)
{
pDrawWnd->GetClientRect(&rect);
templateDC.CreateCompatibleDC(pDC);
visibleDC.CreateCompatibleDC(pDC);
srcDC.CreateCompatibleDC(pDC);
newBmp.CreateCompatibleBitmap(pDC, rect.Width(),
rect.Height());
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);
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &visibleDC,
0, 0, SRCCOPY);
srcDC.SelectObject(pOldSrcBmp);
visibleDC.SelectObject(pOldBmp);
}
CMUILocation*
CMappedUserInterface::GetLocationFromColor(COLORREF crColor)
{
CMUILocation* pLocation = NULL;
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)
{
pDrawWnd->GetClientRect(&rect);
pDC = pDrawWnd->GetDC();
memDC.CreateCompatibleDC(pDC);
srcDC.CreateCompatibleDC(pDC);
newBmp.CreateCompatibleBitmap(pDC, rect.Width(),
rect.Height());
pOldBmp = memDC.SelectObject(&newBmp);
pOldSrcBmp = srcDC.SelectObject(&TemplateBMP);
iOldStretchBltMode = memDC.SetStretchBltMode(HALFTONE);
memDC.StretchBlt(0, 0, rect.Width(), rect.Height(),
&srcDC, 0, 0, bmpTmpl.bmWidth, bmpTmpl.bmHeight,
SRCCOPY);
memDC.SetStretchBltMode(iOldStretchBltMode);
crColorAtPoint = GetPixel(memDC.m_hDC, pt.x, pt.y);
srcDC.SelectObject(pOldSrcBmp);
memDC.SelectObject(pOldBmp);
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;
if (pMui != NULL)
{
delete pMui;
pMui = NULL;
}
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);
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;
crColorAtPoint = pMui->GetTemplateBMPColorAtPoint(point);
CMUILocation* pLocation =
pMui->GetLocationFromColor(crColorAtPoint);
if (pLocation != NULL)
{
}
}
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?
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.