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

显示带滚动条的位图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (80投票s)

2002 年 11 月 10 日

9分钟阅读

viewsIcon

402658

downloadIcon

15827

本文介绍如何在对话框中显示图片,并在需要时添加滚动条来查看整个图像。

引言

在 VC++ 环境中,在对话框中以原始尺寸显示大型位图文件相当困难。然而,使用 StretchBlt() 函数可以将大型位图显示在对话框的预定义区域内。这样做的主要缺点是会损失部分图像的清晰度。本文将介绍如何使用滚动技术在对话框的所需区域以原始尺寸显示大型位图,以查看整个位图。

Demo application

虽然本文的主要目的是显示带有滚动功能的位图,但它也涵盖了以下三个主要目标。

  1. 在运行时将位图加载到对话框中。
  2. 使用滚动技术以原始尺寸显示位图。
  3. 使用双缓冲技术实现无闪烁绘图。

在运行时将位图加载到对话框中

有时,用户可能需要根据 GUI 的要求加载不同的位图。这可以通过调用 LoadImage() 函数来实现。下面将解释其实现过程。

在对话框中放置一个 Picture 控件。将其尺寸调整为您想要的。请记住,您将在该控件中显示位图。大型位图将填充该控件的整个区域,右侧有一个垂直滚动条(如果图像的高度大于控件的高度,则显示滚动条),控件底部有一个水平滚动条(如果图像的宽度大于控件的宽度,则显示滚动条)。小型位图将显示在控件的中心,没有滚动条,控件的左侧和右侧、顶部和底部之间有相等的间隙。因此,请以艺术化的方式放置控件,使您的对话框看起来美观。

获取 Picture 控件的属性。将其 ID 更改为 IDC_STATIC1,类型为 Frame,颜色为 Gray。同时取消选中 Visible 复选框,以便移除其上的勾号。

Properties window

使用 Class Wizard,为 IDC_STATIC1 创建一个类型为 CStatic 的控件变量。将其命名为 m_st1

在对话框的头文件(例如 MyDlg.h)中,添加以下代码:

public:
    CRect rectStaticClient;
    int sourcex, sourcey,offsetx,offsety;
protected:
    CDC m_dcMem;            // Compatible Memory DC for dialog
    HBITMAP m_hBmpOld;    // Handle of old bitmap to save
    HBITMAP m_hBmpNew;    // Handle of new bitmap from file
       BITMAP m_bmInfo;        // Bitmap Information structure
在对话框的实现文件(例如 MyDlg.cpp)中,添加以下代码:
BOOL CMyDlg::OnInitDialog()
{
    
// TODO: Add extra initialization here
    CClientDC dc(this);
     m_dcMem.CreateCompatibleDC( &dc );
return TRUE;  // return TRUE  unless you set the focus to a control
}

重写对话框的 ::OnPaint() 函数的 else 部分。

void MyDlg::OnPaint()
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        //Add the following Code 
        CPaintDC dc(this);
        dc.BitBlt(offsetx,offsety,m_size.cx,m_size.cy, 
                   &m_dcMem, sourcex, sourcey,SRCCOPY);
        CDialog::OnPaint();
    }
}

每当您想在对话框中加载位图时,请编写以下代码:

m_hBmpNew = (HBITMAP) LoadImage(
    AfxGetInstanceHandle(),   // handle to instance
    filename,  // name or identifier of the image .say"C:\\NewFolder\\1.bmp"
    IMAGE_BITMAP,        // image types
    0,     // desired width
    0,     // desired height
    LR_LOADFROMFILE); 

    if( m_hBmpNew == NULL )
    {
        AfxMessageBox("Load Image Failed");
    }
    
    // put the HBITMAP info into the CBitmap (but not the bitmap itself)
    else {
        m_st1.GetClientRect( &rectStaticClient );
        rectStaticClient.NormalizeRect();
        m_size.cx=rectStaticClient.Size().cx;
        m_size.cy=rectStaticClient.Size().cy;
        m_size.cx = rectStaticClient.Width();    // zero based
        m_size.cy = rectStaticClient.Height();    // zero based

        // Convert to screen coordinates using static as base,
        // then to DIALOG (instead of static) client coords 
        // using dialog as base
        m_st1.ClientToScreen( &rectStaticClient );
        ScreenToClient( &rectStaticClient);
        
        m_pt.x = rectStaticClient.left;
        m_pt.y = rectStaticClient.top;
        GetObject( m_hBmpNew , sizeof(BITMAP), &m_bmInfo );
        VERIFY(m_hBmpOld = (HBITMAP)SelectObject(m_dcMem, m_hBmpNew )  );
        offsetx= m_pt.x;
        offsety=m_pt.y;    
        InvalidateRect(&rectStaticClient);

这些代码将在运行时直接将位图显示在 Picture 控件上。请注意,尚未完成滚动功能和对齐调整,因此图像将显示在 Picture 控件的顶部角落,如果其尺寸大于 Picture 控件的尺寸,则会超出 Picture 控件的尺寸。如果图像小于 Picture 控件的尺寸,则不会超出,但也不会居中对齐。下一节将介绍如何实现滚动功能和对齐。

使用滚动功能以原始尺寸显示位图

向对话框添加一个垂直滚动条控件,并将其放置在 Picture 控件的右边缘。将其长度设置为 Picture 控件的高度。向对话框添加一个水平滚动条控件,并将其放置在 Picture 控件的下边缘。将其长度设置为 Picture 控件的宽度。

使用 Class Wizard,为水平和垂直滚动条创建类型为 CScrollBar 的成员变量。例如:

    CScrollBar m_vbar; //For vertical Scroll Bar
    CScrollBar    m_hbar; //For Horizontal Scroll Bar.

您需要为垂直和水平滚动条创建两个 SCROLLINFO 结构来存储滚动条参数,因此请在对话框的头文件中声明两个 SCROLLINFO 结构。

public:
    CRect rectStaticClient;
    int sourcex, sourcey,offsetx,offsety;
    SCROLLINFO horz,vert;

只有当位图的大小大于 Picture 控件的大小时,才需要显示滚动条。因此,请在对话框的 OnInitDialog() 函数中编写以下代码来隐藏滚动条。

BOOL CMyDlg::OnInitDialog()
{
    // TODO: Add extra initialization here
    CClientDC dc(this);
    m_dcMem.CreateCompatibleDC( &dc );
    m_vbar.ShowWindow(false);  //Hide Vertical Scroll Bar
    m_hbar.ShowWindow(false);  //Hide Horizontal Scroll Bar
    return TRUE;  // return TRUE  unless you set the focus to a control
}

当您将位图加载到预定义的 Picture 控件中时,会出现四种情况:

情况 1:加载的位图的宽度和高度均大于 Picture 控件。在这种情况下,需要水平和垂直滚动条来显示整个位图。位图使用滚动技术显示。垂直滚动范围等于位图高度 - Picture 控件高度。位图的高度和宽度通过以下代码获得,该代码已包含在显示位图所需的代码中,在此重新复制如下:

    m_size.cx = rectStaticClient.Width();    // zero based
    m_size.cy = rectStaticClient.Height();    // zero based
    GetObject( m_hBmpNew , sizeof(BITMAP), &m_bmInfo );

最大垂直滚动范围为 m_bmInfo.bmHeight - m_size.cy,最大水平滚动范围为 m_bmInfo.bmWidth - m_size.cx。通过调用以下代码使水平和垂直滚动条可见:

m_hbar.ShowWindow(true);
m_vbar.ShowWindow(true);

情况 2:加载的位图的宽度大于 Picture 控件,而高度等于或小于 Picture 控件。在这种情况下,需要水平滚动条来显示整个位图。位图使用滚动技术显示。水平滚动范围由 m_bmInfo.bmWidth - m_size.cx 给出。

在这种情况下,不需要垂直滚动条,但为了使位图相对于 Picture 控件居中显示,位图应以相对于 Picture 控件左上角的偏移量绘制,该偏移量由下式给出:

offsety = m_pt.y + ((m_size.cy - m_bmInfo.bmHeight)/2);

其中 offsety 是 (x1,y1) (x2,y2) 坐标系中偏移后的 'y1' 坐标,m_pt.y 是原始 'y1' 坐标。

Picture 控件底部会产生 (m_size.cy - m_bmInfo.bmHeight)/2) 的间隙。因此,应使用 MoveWindow() 函数将水平滚动条向上移动 (m_size.cy - m_bmInfo.bmHeight)/2 的距离,如下所述。

m_hbar.MoveWindow(offsetx,offsety+m_bmInfo.bmHeight,m_size.cx,18);

通过调用以下代码使水平滚动条可见,垂直滚动条不可见:

m_hbar.ShowWindow(true);
m_vbar.ShowWindow(false);

情况 3:加载的位图的高度大于 Picture 控件,而宽度等于或小于 Picture 控件。在这种情况下,需要垂直滚动条来显示整个位图。位图使用滚动技术显示。垂直滚动范围由 m_bmInfo.bmHeight - m_size.cy 给出。

在这种情况下,不需要水平滚动条。但为了使位图相对于 Picture 控件居中显示,位图应以相对于 Picture 控件左上角的偏移量显示,该偏移量由下式给出:

offsetx= m_pt.x + ((m_size.cx - m_bmInfo.bmWidth)/2);

其中 offsetx 是 (x1,y1) (x2,y2) 坐标系中偏移后的 'x1' 坐标,m_pt.x 是原始 'x1' 坐标。

Picture 控件右侧边缘会产生 ( (m_size.cx - m_bmInfo.bmWidth)/2) 的间隙。因此,应使用 MoveWindow() 函数将垂直滚动条向左移动 (m_size.cx - m_bmInfo.bmHeight)/2 的距离,使其位于 Picture 控件右边缘的左侧,如下所述。

m_vbar.MoveWindow(offsetx+m_bmInfo.bmWidth,offsety,18,m_size.cy);

通过调用以下代码使垂直滚动条可见,水平滚动条不可见:

m_hbar.ShowWindow(false);
m_vbar.ShowWindow(true);

情况 4:加载的位图的高度和宽度等于或小于 Picture 控件。在这种情况下,不需要垂直和水平滚动条来显示整个位图。位图将居中显示在 Picture 控件中。为了使位图相对于 Picture 控件居中显示,位图应以相对于 Picture 控件左上角的偏移量显示,该偏移量由下式给出:

offsetx= m_pt.x + ((m_size.cx-  m_bmInfo.bmWidth)/2);
offsety= m_pt.y + ((m_size.cy - m_bmInfo.bmHeight)/2);

其中 'offsetx' 是偏移后的 z 坐标,'x1' 坐标位于 (x1,y1) (x2,y2) 坐标系中,m_pt.x 是原始 'x1' 坐标;'offsety' 是偏移后的 y 坐标,'y1' 坐标位于 (x1,y1) (x2,y2) 坐标系中,m_pt.y 是原始 'y1' 坐标。

通过调用以下代码使垂直和水平滚动条不可见:

m_hbar.ShowWindow(false);
m_vbar.ShowWindow(false);

按如下方式填充水平滚动条和垂直滚动条的 SCROLLINFO 结构。

//Horizontal Scroll Info Structure
horz.cbSize = sizeof(SCROLLINFO);
horz.fMask = SIF_ALL;
horz.nMin = 0;
horz.nMax = m_bmInfo.bmWidth-m_size.cx;
horz.nPage =0;
horz.nPos = 0;
horz.nTrackPos=0;
m_hbar.SetScrollInfo(&horz);

//Vertical Scroll Info Structure
vert.cbSize = sizeof(SCROLLINFO);
vert.fMask = SIF_ALL;
vert.nMin = 0;
vert.nMax = m_bmInfo.bmHeight-m_size.cy;
vert.nPage = 0;
vert.nTrackPos=0;
m_vbar.SetScrollInfo(&vert);

现在,通过使 Picture 控件失效来显示图片。

InvalidateRect(&rectStaticClient);

请记住,根据加载图像的需求,滚动条的位置可能会发生变化。因此,在将另一张位图加载到对话框之前,请释放当前位图所在的内存,并将滚动条的位置重置为其原始位置,即在设计对话框时放置滚动条的位置,通过调用以下函数:

    if(m_hBmpNew != NULL )
        DeleteObject(m_hBmpNew);         //Release Memory holding Bitmap

    // Reset position of Vertical Scroll Bar
    m_vbar.MoveWindow(offsetx+m_size.cx,offsety,18,m_size.cy);     

    // Reset position of Horizontal Scroll Bar
    m_hbar.MoveWindow(offsetx,offsety+m_size.cy,m_size.cx,18);    

以下代码总结了上述内容:

    if(m_hBmpNew != NULL )
        DeleteObject(m_hBmpNew);    //Release Memory holding Bitmap

    sourcex=sourcey=0;        // Set starting Position of Source Bitmap to 
                              //be copied to (0,0)
    m_hBmpNew =    (HBITMAP) LoadImage(AfxGetInstanceHandle(), // handle to instance
        "Path of Bitmap",  // name or identifier of the image (say "C:\\bitmap.bmp")
        IMAGE_BITMAP,        // image types
        0,     // desired width
        0,     // desired height
        LR_LOADFROMFILE); 

    if( m_hBmpNew == NULL ) {
        AfxMessageBox("Load Image Failed");
    }    
    // put the HBITMAP info into the CBitmap (but not the bitmap itself)
    else {
        m_st1.GetClientRect( &rectStaticClient );
        rectStaticClient.NormalizeRect();
        m_size.cx=rectStaticClient.Size().cx;
        m_size.cy=rectStaticClient.Size().cy;
        m_size.cx = rectStaticClient.Width();    // zero based
        m_size.cy = rectStaticClient.Height();    // zero based

        // Convert to screen coordinates using static as base,
        // then to DIALOG (instead of static) client coords 
        // using dialog as base
        m_st1.ClientToScreen( &rectStaticClient );
        ScreenToClient( &rectStaticClient);
        
        m_pt.x = rectStaticClient.left;
        m_pt.y = rectStaticClient.top;
        GetObject( m_hBmpNew , sizeof(BITMAP), &m_bmInfo );
        VERIFY(m_hBmpOld = (HBITMAP)SelectObject(m_dcMem, m_hBmpNew )
    );

    offsetx= m_pt.x;
    offsety=m_pt.y;
    
    // Reset position of Vertical Scroll Bar
    m_vbar.MoveWindow(offsetx+m_size.cx,offsety,18,m_size.cy);    

    // Reset position of Horizontal Scroll Bar
    m_hbar.MoveWindow(offsetx,offsety+m_size.cy,m_size.cx,18);    
    horz.cbSize = sizeof(SCROLLINFO);
    horz.fMask = SIF_ALL;
    horz.nMin = 0;
    horz.nMax = m_bmInfo.bmWidth-m_size.cx;
    horz.nPage =0;
    horz.nPos = 0;
    horz.nTrackPos=0;
    if(m_bmInfo.bmWidth<=m_size.cx)
    {
        if((m_size.cx-m_bmInfo.bmWidth)==0)
            offsetx= m_pt.x;
        else
            offsetx= m_pt.x+((m_size.cx-m_bmInfo.bmWidth)/2);
        m_vbar.MoveWindow(offsetx+m_bmInfo.bmWidth,offsety,18,m_size.cy);
        m_hbar.ShowWindow(false);
    }
    else
        m_hbar.ShowWindow(true);
    
    m_hbar.SetScrollInfo(&horz);
    vert.cbSize = sizeof(SCROLLINFO);
    vert.fMask = SIF_ALL;
    vert.nMin = 0;
    vert.nMax = m_bmInfo.bmHeight-(m_size.cy);
    vert.nPage = 0;
    vert.nTrackPos=0;
    if(m_bmInfo.bmHeight<=m_size.cy)
    {
        if((m_size.cy-m_bmInfo.bmHeight)==0)
            offsety= m_pt.y;
        else
            offsety= m_pt.y+((m_size.cy-m_bmInfo.bmHeight)/2);
        m_hbar.MoveWindow(offsetx,offsety+m_bmInfo.bmHeight,m_size.cx,18);
        m_vbar.ShowWindow(false);
    }
    else
        m_vbar.ShowWindow(true);

    m_vbar.SetScrollInfo(&vert);

    InvalidateRect(&rectStaticClient);
}

现在,您的位图已准备好在对话框中显示(如果需要,带有滚动条)。但它仍然无法滚动以显示其余部分。我们需要处理 WM_VSCROLLWM_HSCROLL 消息,以便根据滚动条位置重新绘制位图。

使用 Class Wizard,处理 WM_VSCROLLWM_HSCROLL 消息,并在它们的处理程序中编写以下代码:

void CMyDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
    // TODO: Add your message handler code here and/or call default
    switch (nSBCode)
    {
    case SB_TOP:
        sourcey = 0;
        break;
    case SB_BOTTOM:
        sourcey = INT_MAX;
        break;
    case SB_THUMBTRACK:
        sourcey = nPos;
        break;
    }

    m_vbar.SetScrollPos(sourcey);
    InvalidateRect(&rectStaticClient);
    CDialog::OnVScroll(nSBCode, nPos, pScrollBar);
}

void CMyDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
    // TODO: Add your message handler code here and/or call default
    switch (nSBCode)
    {
    case SB_TOP:
        sourcex = 0;
        break;
    case SB_BOTTOM:
        sourcex = INT_MAX;
        break;
    case SB_THUMBTRACK:
        sourcex= nPos;
        break;
    }    
    m_hbar.SetScrollPos(sourcex);
    InvalidateRect(&rectStaticClient);
    CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}

现在您可以滚动您的位图,并通过水平和垂直滚动查看其其余部分。仍然存在一个问题。滚动位图时屏幕会持续闪烁。现在是解决此问题的另一种技术。这是此项目的最终润色——“双缓冲技术实现无闪烁绘图”,我们将采用这种技术。

使用双缓冲技术实现无闪烁绘图

为了消除绘图时的闪烁,您需要在内存 DC 上绘制所有内容,然后使用 BitBltStretchBlt 函数将其复制到实际 DC。此技术称为双缓冲。本文使用的绘图技术就是双缓冲。前面部分已对此进行了解释。其次,您需要重写 OnEraseBackground() 事件。该事件的默认实现会用 BackColor 属性的当前值清除控件的背景。但是,并非总是需要重绘控件的整个区域,不必要的重绘可能会导致闪烁。在通过 InvalidateRect(&rectStatcClient) 重绘对话框时,请绕过 OnEraseBackground() 事件。这可以通过设置一个全局变量来实现。声明一个类型为 BOOL 的全局变量,例如 BOOL erase,并将其初始化为 false。映射 WM_ERASEBKGND 消息并重写如下:

BOOL MyDlg::OnEraseBkgnd(CDC* pDC) 
{
    // TODO: Add your message handler code here and/or call default
    if(erase)
        return false;
    else
    return CDialog::OnEraseBkgnd(pDC);
}

在调用 InvalidateRect & rectStatcClient 函数之前,将变量 'erase' 设置为 true。现在将绕过 OnEraseBkgnd()

OnPaint 函数的末尾将 'erase' 标志重置为 false。这将取消绕过 OnEraseBkgnd(pDC) 事件,并且当其他事件(除了 InvalidateRect( &rectStaticClient ))发送 WM_ERASEBKGND 时,背景将被擦除。

    else
    {    
        CPaintDC dc(this);
         dc.BitBlt(offsetx,offsety,m_size.cx,m_size.cy, 
                    &m_dcMem, sourcex, sourcey,SRCCOPY);
        erase=false;
        CDialog::OnPaint();
    }

我希望您现在能够使用滚动功能在对话框中绘制位图。请查看演示项目并从中学习更多知识。

注意:此代码已在 Windows 2000 平台上进行测试,并且运行正常。我们随时欢迎您宝贵的建议和更正。有关“双缓冲”、“MoveWindow()”和“BitBlt()”的更多详细信息,请参阅 MSDN 文档。

© . All rights reserved.