如何在基于对话框的 MFC 应用程序上启用滚动?

How to enable scrolling on Dialog Based MFC app?

很抱歉这个问题,但我是 MFC 的初学者。我有空 MFC 基于对话框的应用程序

我正在应用程序中加载图像,如果图像比对话框大,则无法启用滚动。这是我的 displayImage() 方法,其中 m_imgCImage:

void CTestAppDlg::displayImage(CString path)
{
    m_img.Load(path.GetBuffer());

    CRect rectWindow;
    GetClientRect(&rectWindow);

    m_rcImg.left = rectWindow.left;
    m_rcImg.right = rectWindow.left + m_img.GetWidth();
    m_rcImg.top = rectWindow.top;
    m_rcImg.bottom = rectWindow.top + m_img.GetHeight();

    CDC* pDC = GetDC();
    m_img.StretchBlt(pDC->GetSafeHdc(), 0, m_toolbar.GetRowHeight() + 1, m_rcImg.Width(), m_rcImg.Height(), 0, 0, m_img.GetWidth(), m_img.GetHeight(), SRCCOPY);
}

我试过这样解决问题:

  1. 启用对话框滚动:

试了下,显示了滚动条,但是还是不能滚动图片

  1. 我从这个 link 中找到了实施 ScrollHelper class 的其他解决方案。我通过在对话框构造函数中调用它,将滚动助手附加到对话框:

    m_scrollHelper->AttachWnd(this);

但结果是我可以滚动对话框 Window 而图像仍然没有滚动。我无法将它附加到 CImage,因为它可以附加到 CWnd 或 CDialog 派生的 class.

欢迎任何建议或提示。提前致谢。

您使用基于对话框的应用程序有什么原因吗?这是非常有限的...

Document-View 架构提供的 CScrollView 将为您完成一切。

https://docs.microsoft.com/en-us/cpp/mfc/reference/cscrollview-class?view=msvc-160

您可以使用图片控件和滚动组合来满足您的需要。

以下是关于Displaying Bitmap with Scrolling的项目。这是一篇展示如何在对话框中显示图片,并在需要查看整个图像的地方添加滚动条的文章。你可以参考一下。

在对话框中放置图片控件。根据需要调整其大小。请记住,您将在此控件中显示位图。大位图将限制在该控件的整个区域,右侧有一个垂直滚动条(如果图像的高度大于该控件的高度,将显示滚动条),底部有一个水平滚动条此控件(如果图像的宽度大于此控件的宽度,将显示滚动条)。小位图将显示在该控件的中心,没有滚动条,与控件的左右、顶部和底部有相等的间隙。因此,让您的控件具有艺术感,使您的对话框外观漂亮。

取图片控件的属性。将其 ID 更改为 IDC_STATIC1,类型为 Frame,颜色为 Gray。同时取消选中可见复选按钮,以便从中删除刻度线。

使用 Class 向导,为 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
}


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

这么多代码将在 运行 时间内将位图直接显示到图片控件上。请记住滚动功能和对齐调整尚未完成,因此图像将显示在图片控件的顶角,如果它的大小大于图片控件的大小,它将被裁剪到图片控件的尺寸。如果图像小于图片控件的大小,它将在不剪裁的情况下显示,但不会居中对齐。以下部分描述了如何实现滚动功能和对齐。

使用滚动以原始大小显示位图

向您的对话框添加一个垂直滚动条控件,并将其放置在您的图片控件的右边缘。使其长度与图片控件的高度一致。将水平滚动条控件添加到您的对话框并将其放置在您的图片控件的底部边缘。使其长度与图片控件的宽度一致。

使用 Class 向导,为水平和垂直滚动条创建 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;

仅当位图的大小大于图片控件的大小时才需要显示滚动条。因此,最初通过在对话框的 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
}

将位图加载到预定义图片控件时会出现四种情况。他们是:

情况一:加载的位图宽高都大于图片控件。在这种情况下,水平和垂直滚动条都是显示整个位图所必需的。使用滚动技术显示位图。垂直滚动范围等于图片控件的bitmap-height的高度。位图的高和宽是通过以下代码获取的,该代码合并在显示位图所需的代码中,这里再次转载为:

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:THe加载的位图宽度大于图片控件,高度等于或小于图片控件。在这种情况下,水平滚动条是显示整个位图所必需的。使用滚动技术显示位图。水平滚动范围由 m_bmInfo.bmWidth-m_size.cx.

给出

在这种情况下不需要垂直滚动条,但为了显示相对于图片控件集中的位图,应在距图片控件顶角由[=33=给出的偏移处绘制位图]

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

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

图片控件的底部也产生了(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);

情况三:加载的位图高度大于图片控件,宽度等于或小于图片控件。在这种情况下,垂直滚动条是显示整个位图所必需的。使用滚动技术显示位图。垂直滚动范围由 m_bmInfo.bmHeight-m_size.cy.

给出

在这种情况下不需要水平滚动条,但是为了显示相对于图片控件集中的位图,位图应该显示在距图片控件顶角的偏移量处:

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

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

((m_size.cx - m_bmInfo.bmWidth)/2)的间隙也从图片控件的最右侧产生。因此垂直滚动条应该使用 MoveWindow( ) 函数从图片控件的右边缘向左移动一个量 (m_size.cx - m_bmInfo.bmHeight)/2 ,如下所述。

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

通过调用

使垂直滚动条可见,水平滚动条不可见
m_hbar.ShowWindow(false);
m_vbar.ShowWindow(true);

情况4:加载位图的高和宽等于或小于图片控件的高和宽。在这种情况下,不需要垂直和水平滚动条来显示整个位图。位图在图片控件中集中显示。为了相对于图片控件集中显示位图,位图应显示在距图片控件顶角的偏移量处:

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

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

InvalidateRect(&rectStaticClient);

请记住,根据您加载的图像的要求,滚动条的位置可能会发生变化。因此,在您的对话框中显示另一个位图之前,释放保存当前位图的内存并将滚动条的位置重置为其原始位置,即您在对话框设计期间放置它们的位置,通过调用 如果(m_hBmpNew!=空) DeleteObject(m_hBmpNew); //释放持有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);

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

使用 Class 向导,处理 WM_VSCROLL 和 WM_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 上,然后使用BitBlt 或StretchBlt 函数将其复制到真正的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) 事件的绕过,当 WM_ERASEBKGND 由 InvalidateRect( &rectStaticClient ) 以外的其他事件发送时,背景将被擦除。

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