65.9K
CodeProject 正在变化。 阅读更多。
Home

映射用户界面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (9投票s)

2003年11月4日

7分钟阅读

viewsIcon

56571

downloadIcon

2894

设计非矩形热点或拥有大量控件的窗口的用户界面。

目录

介绍

映射用户界面(MUI)是一种用户界面,其窗口上的项目由一个简单的图形“地图”定义。该地图是一个带有为每个用户界面项分配的唯一颜色的分界位图蒙版,其余部分为黑色。有关地图示例,请参阅提供的屏幕截图。地图上点的颜色被检索并用于查找关联的对象,该对象使用RGB颜色作为唯一标识符。

该代码很简单,并且定义地图所需的工作量比其他方法(如为每个UI元素定义区域或矩形)少。它确实需要某种数据库,可以是平面文件、XML或硬编码数组,用于通过唯一颜色查找对象数据。在示例应用程序中,使用了逗号分隔文件。

你可能会问:“在什么情况下我想使用这种方法?”如果您有一个要用作用户界面基础的大型图形,例如世界地图或机械图,并且希望用户能够单击不同区域以获取更多信息,那么MUI将是您的选择。这让我们想到CP用户提出的一個具体问题,也是这个想法的初衷。

问题

早在2003年9月22日,b_girl在Visual C++讨论板上 提出了这个问题 [^]

我开发的界面的一部分需要某种交互式的元素周期表。我不完全确定如何进行,我想知道这里是否有人有什么好主意,或者知道在哪里可以找到相关信息。我需要它看起来像传统的周期表,但是用户可以点击任意数量的元素,并且他们选择的元素的编号将进入一个列表。我唯一能想到的是为每个元素创建一个按钮(这有很多按钮!!),每次单击按钮时,元素编号就会添加到列表中或从列表中删除。有人有什么更好的主意可以分享吗?

收到了一些好主意,但在我别扭的思维方式下,我构思了映射用户界面(MUI) 概念 [^]。当时,我认为这是一个独特的想法,后来才发现Paul DiLascia早在6年前就想到了。稍后会详细介绍。

多重解决方案

正如我所说,收到了几个好主意,包括 Terry O'Nolley [^] 的 GDI解决方案 [^],他也为此写了一篇好文章。解决这个特定问题的其他可能方案包括使用网格控件,以及使用带有矩形边界元素的大位图。这些都是周期表的不错解决方案,但我认为MUI易于实现,并且其优点大于缺点。

优点和缺点

MUI
优点 缺点
比其他方法代码量少 需要图形能力
易于维护(只需更新地图位图和数据库) 如果宽高比不正确,则文本会显得杂乱
可以使用StretchBlt函数进行缩放  
可以定义非矩形UI元素  

映射用户界面

如果您有一个适合使用MUI的候选应用程序,您需要做的第一件事是找到一个好的图形作为主显示图片。一旦找到完美的图片,您就需要制作地图位图。我使用Paint Shop Pro(MS Paint也可以)为每个UI项目“剪切”形状。首先,复制一张图片并用PSP打开。然后选择一个唯一的颜色作为对象,并将其设为背景色,然后使用套索工具在UI项目周围进行描边。完成描边后,按Delete键,该项目将被填充为一种颜色。对您想要处理的每个UI项目重复此操作。当所有项目都有了唯一颜色后,您需要将图片的其余部分变为黑色。在PSP中,按住Shift键并单击每个有颜色的项目,这将选中它们,然后转到Selections菜单并选择Invert。这将选择除UI项目之外的所有内容。现在将背景色更改为黑色(RGB(0,0,0,))并按Delete。除了UI项目之外,所有内容都应为黑色。您的地图图形现在已完成。

这很简单,现在是最繁琐的部分,我们需要找到每个UI项目的RGB值,并将其添加到数据库或平面文件中的颜色值中。这一步留给您,但我使用Excel和一个简单的应用程序来完成,该应用程序使用GetPixel从地图中提取RGB值。请参阅演示源zip文件中的CSV文件作为示例。顺便说一句:Excel非常适合这类工作。

现在您有了地图和数据库,让我们更详细地看一下一些代码。

核心代码

我定义了一些基类来帮助简化MUI的使用。CMUILocation是UI项目对象继承的基类。每个地图“位置”始终具有一个颜色(唯一标识符)和一个名称。派生类定义了每个对象类型的次要属性。

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是一个抽象基类,负责绘图以及通过颜色检索“位置”。CArray用于存储CMUILocation以供快速检索。LoadDataFromFile函数将位置加载到locationList CArray中。它在派生类中实现,因为位置对象可以具有不同的属性。例如,Element对象具有颜色、名称、原子质量和原子序数属性,而演示应用程序中的UnitedStates对象则具有颜色、名称、缩写、人口和州鸟属性。

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;
}

在演示应用程序的ChildView中,SetMUI函数用于在三个不同的可用MUI之间切换。pMui是指向CMappedUserInterface对象的指针,但由于该类是抽象的,您无法实例化该类型的对象,必须实例化一个派生类并将其转换为基类。这对于演示应用程序效果很好,因为我想为每个不同的可用MUI使用一个CMappedUserInterface对象。我不需要为每个不同的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();
}

一旦设置了MUI(在演示中,它在OnCreate消息处理程序中设置),我们就可以在OnPaint处理程序中调用DrawMUI成员函数。MUI会自行绘制,所以此时这就是绘图所需的一切。

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

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

OnLButtonDown演示了如何从CPoint检索CMUILocation对象。

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
    }
}

MUI的应用

MUI并不适用于所有应用程序或窗口,只适用于那些具有非矩形热点,或拥有数量庞大到难以管理的控件的应用程序或窗口。一些潜在用途包括:数字世界或国家地图、复杂机械插图,甚至人体图。您还能想到多少种用途?

致谢

正如我之前提到的,我并不是第一个想到这个主意的人。 Paul DiLascia [^],曾为 MSJ [^] 和 MSDN [^] 杂志撰稿,早在1997年3月的 MSJ期刊 [^] 就写过关于它的文章。我是在查找如何在使用控件之外的热点上使用工具提示时偶然发现这篇论文的。我想聪明人想的一样,对吧?总之,他的文章写得很好,我不想夺走他的功劳。工具提示的代码源自他的文章,但其余部分是我自己的。

© . All rights reserved.