有些控件不是绘制的,似乎是随机的

Some controls are not drawing, seemingly at random

提问人:Daniel Bauer 提问时间:5/7/2019 最后编辑:Peter MortensenDaniel Bauer 更新时间:7/2/2019 访问量:356

问:

我正在尝试为自己编写一个小的 MFC 应用程序,以测试我正在训练的一些 AI。

因此,我添加了一个图片控件和一个静态控件,我可以在主窗口的 OnPaint() 方法中自由绘制内容。

当只绘制一次我的应用程序时,它似乎可以工作,但我现在添加了一个循环,该循环在停止之前多次执行 OnPaint()。

在这个循环中,其他一些控件没有显示出来,例如,我所有的按钮都消失了,有些滑块甚至有时丢失了,但其他时候,它们就在那里。

我的代码是这样的:

void CKiUebung1Dlg::OnBnClickedButtongo()
{
    m_bisGoing = true;
    OnPaint();
    if(m_fDiagramData.size() <= 0)
    {
        m_fDiagramData.push_back((float)rand() / RAND_MAX);
        InvalidateRect(NULL, TRUE);
    }
    OnPaint();
    for(int i(9); i >= 0; --i)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        m_fDiagramData.push_back((float)rand() / RAND_MAX);
        InvalidateRect(NULL, TRUE);
        OnPaint();
    }
    m_bisGoing = false;
    OnPaint();
}
void CKiUebung1Dlg::OnPaint()
{
    if(IsIconic())
    {
        CPaintDC dc(this); // Gerätekontext zum Zeichnen

        SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

        // Symbol in Clientrechteck zentrieren
        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;

        // Symbol zeichnen
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialogEx::OnPaint();
    }
    {
        constexpr const int border = 5;
        CPaintDC dc(&m_cDiagram);
        CRect l_cPos;
        m_cDiagram.GetClientRect(&l_cPos);
        const int width(l_cPos.Width() - border * 2 - 2), height(l_cPos.Height() - border * 2 - 12);
        const int numPoints(m_fDiagramData.size());
        POINT* points(new POINT[numPoints]);
        for(int i(numPoints - 1); i >= 0; --i)
        {
            const int
                x((float)i / (numPoints - 1) * width + border + 1),
                y(height - m_fDiagramData[i] * height + border + 9);
            points[i] = { x,y };
        }
        dc.Polyline(points, numPoints);

        static CString going(_T(" "));
        if(m_bisGoing) { going += _T("."); if(going.GetLength() > 300) going = _T(" ."); }
        else going = _T(" ");
        float fprog(0); if(m_fDiagramData.size() > 0) fprog = m_fDiagramData.back();
        CString prog; prog.Format(_T("Progress %03.2f%%"), fprog * 100); if(m_bisGoing) prog += going;
        m_cDiagram.SetWindowTextW(prog);

        m_cDiagram.RedrawWindow();

        delete[] points;
    }
}

这是循环未运行时的样子:

This is how it looks when the loop isn't running

这是循环运行时的样子:

This is how it looks when the loop is running

MFC系列

评论

2赞 IInspectable 5/7/2019
“多次执行 OnPaint() 的循环” - 这听起来很奇怪。 通常是一个消息处理程序,而不是您应该调用的常规函数。当您在消息处理程序中时,所有消息处理都会停止,即没有控件的处理程序运行。您可以通过在控件父项上设置样式来限制损害,但您确实需要解决核心问题。如果没有看到最小的可重复示例,就很难提供解决方案。OnPaintWM_PAINTWS_CLIPCHILDREN
0赞 Daniel Bauer 5/8/2019
我添加了一些代码来伪造应用程序背后的操作。如果我不应该直接调用它,我可以用什么替换 OnPaint()?

答:

3赞 Barmak Shemirani 5/8/2019 #1

CWnd::OnPaint 是对WM_PAINT消息的响应,不应直接调用。

WM_PAINTcalls ,调用 CPaintDC dc(this),而 CPaintDC dc(this) 又调用 BeginPaint/EndPaint API。此消息+响应序列应保持原样。CWnd::OnPaint

因此,必须出现一次 - 而且只出现一次 - 内部,而不是其他任何地方。覆盖如下:CPaintDC dc(this)OnPaintOnPaint

void CMyDialog::OnPaint()
{
    CDialogEx::OnPaint(); //this will call CPaintDC dc(this);

    //optional: 
    CClientDC dc(this); //CClientDC can be used anywhere in a valid window
    //use dc for drawing
}

//or
void CMyDialog::OnPaint()
{
    CPaintDC dc(this); 
    //use dc for drawing
}

您也不需要过时的条件。if (IsIconic()) {...}

若要强制窗口自行重绘,请调用 Invalidate() (与 InvalidateRect(NULL, TRUE) 相同)

InvalidateRect(NULL, TRUE)是重新绘制窗口的请求。系统将查看此请求,并在有机会时向该窗口发送消息。因此,调用可能无法按照您期望它在顺序程序中工作的方式进行处理。例如,连续第二次调用 将没有任何效果。窗口已被标记为要更新。WM_PAINTInvalidateRectInvalidateRect

 for(int i(9); i >= 0; --i)
 {
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    m_fDiagramData.push_back((float)rand() / RAND_MAX);
    InvalidateRect(NULL, TRUE);
    OnPaint();
 }

OnPaint()应该从上面的代码中删除。尽管如此,动画在单个线程中是不可能的(至少不能以这种方式)。程序正忙于循环,无法处理其他消息。WM_PAINT

因此,您需要一个额外的线程,或者简单地使用 ,并响应 / 用于动画。例:SetTimerON_WM_TIMER()OnTimer

int counter = 0;

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_WM_PAINT()
    ON_WM_TIMER()
    ...
END_MESSAGE_MAP()

void CMyDialog::OnPaint()
{
    CPaintDC dc(this);
    CString s;
    s.Format(L"%02d", counter);
    dc.TextOut(0, 0, s);
}

void CMyDialog::animate()
{
    counter = 0;
    SetTimer(1, 1000, NULL);
}

void CMyDialog::OnTimer(UINT_PTR n)
{
    if(n == 1)
    {
        Invalidate(); //force repaint
        counter++;
        if(counter == 10)
            KillTimer(1);
    }
}

评论

2赞 IInspectable 5/8/2019
由于您提供了单线程的动画方法,因此您可能需要重新审视声称这是不可能的段落。调用也不会“强制”重新绘制。这是一个请求。关于如何使部分或全部窗口无效以生成消息的部分以过于简单的方式呈现。 当没有更高优先级的消息排队时,将按需生成消息。由此得出的推论是,可以将多个调用合并为一条消息。Invalidate()WM_PAINTWM_PAINTInvalidate()WM_PAINT
0赞 Barmak Shemirani 5/9/2019
@IInspectable,好吧,我改变了措辞。
1赞 IInspectable 5/9/2019
如何使用保证不与任何其他计时器 ID 冲突的计时器 ID 调用 SetTimer?只要您控制窗口,这很简单:添加一个实例成员(例如)并使用其地址作为计时器 ID。直到 2015 年 9 月,我从未考虑过 ID 是指针大小的事实。int m_AnimationTimerId;
1赞 zett42 5/9/2019
@IInspectable 好主意。当然,只有当每个人都同意以这种方式创建计时器 ID 时,这才有效。@Barmak 应提交请求,以从 MFC 应用程序向导生成的代码中删除。代码在 Win95 中已经过时了。if (IsIconic()) {...}
3赞 Constantine Georgiou 5/9/2019 #2

您似乎难以理解无效/绘画的工作原理。 您应该首先阅读的文档是: 绘画和绘图

虽然许多开发人员建议仅在处理中绘制(在 MFC 中),但这并不总是最佳解决方案,因为此消息是低优先级的,绘制可能不是即时的(有“断断续续”的感觉),并且您可能会得到“闪烁”效果。WM_PAINTOnPaint()

相反,我有时会推荐绘画和绘画的混合:

  • 在加工中使用油漆。这应该绘制整个工作区(或者只绘制其中的无效部分,如果你想要一个更“优化”的实现)。请注意,除了以编程方式使窗口无效外,由于移动、调整大小、取消隐藏窗口等原因,可能会因部分或全部工作区无效而收到消息。因此,为了响应消息,您应该执行完全重绘,即要显示的所有项目。WM_PAINTWM_PAINTWM_PAINT
  • 在应用程序繁忙时(不等待接收“异步”消息),使用绘图来显示您希望立即显示的更改。请注意,这些也应该在处理中,因此您最好编写一些绘图/绘画例程,将 (或) 作为参数(以及所需的任何其他参数),并从函数(传递那里)和所需的其他绘图操作(通过调用传递获取)调用它们。WM_PAINTWM_PAINTHDCCDC*OnPaint()ClientDCCDC*GetDC()

所以,让我分享一下我(很久以前)写的一个应用程序的经验。它是一个图像显示/操作(除其他外)应用程序,以自定义格式处理图像,并使用一个特殊的库,这相当“慢”,因为它只提供了一个在设备上下文中显示图像的功能(这包括可能的裁剪、调整、调整大小等,这些都是 CPU 成本高昂的操作)。这是一张图片:

enter image description here

您可以看到用户正在执行选择。应用程序必须显示图像,可能还有其顶部的选择矩形,当然就是这样。一个“简单”(尽管在技术上“正确”)的实现是调用或响应每个鼠标移动消息(在选择时)。这将导致完全重绘(这是“好的”),但由于图像库速度慢,也会遇到性能问题:如果您在无效(请求立即刷新)后也调用,性能将很慢(必须重新处理/重新显示图像),如果没有,刷新只会在一段时间后(明显)发生。这是通过使用 drawign(不是绘画)来响应消息来解决的:没有在那里无效,而是只绘制选择矩形(在恢复由上一个选择消息修改的部分之后 - 我只备份/恢复框架的四个边,而不是整个矩形)。因此,尽管库速度较慢,但应用程序响应迅速,操作流畅,并且正确显示图像和选择,即使您在跟踪选择时切换到另一个应用程序然后返回它(虚线)。OnPaint()Invalidate()InvalidateRect()UpdateWindow()WM_MOUSEMOVE

关于您的实现的一些说明和建议(它有很多问题):

  • 正如其他成员所指出的,你不能给自己打电话。尤其是之后的那些电话完全没有意义。相反,如果您想要立即更新,请致电 。OnPaint()Invalidate()UpdateWindow()
  • Imo 在 中执行计算是不行的,我的意思是那些点计算(尽管在您的情况下,计算相当微不足道)。 应该只显示在代码的另一部分计算的数据。OnPaint()OnPaint()
  • 此外,设置文本和从内部重新绘制也是不行的(可能会导致额外的绘画请求)。最好将它们移到 .m_cDiagramOnPaint()OnBnClickedButtongo()
  • 您无需使整个工作区失效(尤其是擦除)以重新绘制某些控件,而只需使这些控件失效。请记住,该函数是阻塞的,并且在循环运行时不会发送和处理消息。sleep_for()WM_PAINT
  • 顺便说一句,考虑一种非阻塞方法,例如使用计时器,正如 Shemirani @Barmak建议的那样。或者,可以通过自己运行消息循环来编写“非blocing”(将部分代码放入并对其进行修改)。sleep()CWinApp::Run()
  • 由于您有一个对话框并创建了单独的控件来显示数据,因此使用不是一个好的实现,因为它会影响(绘制)整个工作区。它主要用于像 or 这样的类(或一般的自定义绘画)。您在对话框的表面上绘制图形,并且必须执行计算才能获得坐标(顺便说一句,您可以使用然后代替),但最好使用所有者绘制的控件(在上面绘制/绘制图形),这并不难,您只需要响应绘制请求(就像在 中一样), 您获取的设备上下文只能在控件上绘制,而不能在对话框上绘制;坐标相对于控件的工作区,从 (0,0) 开始。OnPaint()CViewCScrollViewCWndm_cDiagramGetWindowRect()ScreenToClient()OnPaint()

希望这会有所帮助

评论

2赞 IInspectable 5/9/2019
如果从处理程序调用,则不会生成 no(鼠标输入的优先级高于绘制消息)。呈现焦点矩形的“正确”方法是调用 DrawFocusRect。它有一个特别有趣的功能:当它执行破坏性操作时,可以通过再次执行相同的操作来恢复初始图像。这允许您绘制焦点矩形,而无需对图像进行完全重新绘制。Invalidate()WM_MOUSEMOVEWM_PAINT
0赞 Daniel Bauer 5/9/2019
非常实用的答案。我将用它来纠正我的程序。只有关于非阻塞计时器的部分不适用,因为这是出于 MCVE 原因在后台模拟我的计算的部分,这显然必须是阻塞。很抱歉没有说清楚。
0赞 Constantine Georgiou 5/17/2019
@IInspectable,感谢您的评论。 如果用户没有移动鼠标太快,消息确实可以在它们之间发送和处理。尽管更新会不稳定,但该方法是“正确的”,因为它最终会正确绘制图像,即它是一个“快速”的,是的,笨拙的,但不是“肮脏”的实现。至于绘制矩形,确实,我一开始尝试过反转(尽管我使用的是 rop 参数而不是 ),(续)WM_PAINTWM_MOUSEMOVEBitBlt()DSTINVERTDrawFocusRect()
0赞 Constantine Georgiou 5/17/2019
(续) 但是对于这种灰度图像,结果并不好,因为大多数时候,反面是另一个灰度值,与原始值非常接近,并且选择矩形的阴影不同。在几乎看不见和丑陋之间变化。我的方法并不比使用更复杂,因为这也需要在绘制新矩形之前恢复以前的矩形 - 当然,我必须在绘制框架之前备份框架,但这仅在单个点(就在绘制之前)完成,因此没有额外的复杂性。DrawFocusRect()
1赞 IInspectable 5/17/2019
之后恢复映像需要跟踪单个结构(即 16 个字节)。它只涉及那些改变的像素的痛苦。这与每次鼠标移动时都传输数十和数百千字节有很大不同。DrawFocusRectRECT