有些控件没有绘制,看似随机
Some controls are not drawing, seemingly at random
我正在尝试为自己编写一个小的 MFC 应用程序,以测试我正在训练的一些 AI。
所以我添加了一个图片控件和一个静态控件,我可以在主 Window 的 OnPaint() 方法中自由地绘制东西 Window。
当我的应用程序只绘制一次时它似乎工作正常,但我现在添加了一个在停止之前多次执行 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;
}
}
这是没有循环时的样子 运行:
这是循环 运行:
时的样子
CWnd::OnPaint
is a response to WM_PAINT
消息,不应直接调用。
WM_PAINT
调用 CWnd::OnPaint
,后者调用 CPaintDC dc(this)
, which in turns calls BeginPaint
/EndPaint
API。这个消息+响应的顺序应该保持原样。
因此 CPaintDC dc(this)
必须在 OnPaint
中出现一次 - 并且只能出现一次 - 而不是在其他任何地方。覆盖 OnPaint
如下:
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()) {...}
条件。
要强制 window 重绘自身,请调用 Invalidate()
(same thing as InvalidateRect(NULL, TRUE)
)
InvalidateRect(NULL, TRUE)
请求重绘 window。系统将查看此请求,并在有机会时向 window 发送 WM_PAINT
消息。因此,对 InvalidateRect
的调用可能不会按照您期望它在顺序程序中工作的方式进行处理。例如,第二次连续调用 InvalidateRect
不会有任何效果。 Window 已标记为要更新。
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
和其他消息。
所以你需要一个额外的线程,或者干脆使用SetTimer
,并响应ON_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);
}
}
您似乎无法理解 invalidating/painting 的工作原理。
您应该首先阅读的文档是:
Painting and Drawing
虽然许多开发人员建议仅在 WM_PAINT
处理(在 MFC 中为 OnPaint()
)中绘制,但这并不总是最佳解决方案,因为此消息是 low-priority,绘制可能不是立竿见影(有 "choppy" 的感觉),你可能会得到 "flickering" 的效果。
相反,我有时会推荐混合绘画:
- 在
WM_PAINT
处理中应用绘画。这应该绘制整个客户区(或者如果你想要更多 "optimized" 实现,则只绘制它的无效部分)。请注意,WM_PAINT
消息可能是由于 window 的移动、调整大小、取消隐藏等原因导致部分或全部客户区无效而收到的,除了以编程方式使它无效之外。因此,为了响应 WM_PAINT
消息,您应该执行完全重绘,即您要显示的所有项目。
- 在应用程序繁忙时(不等待收到 "asynchronous"
WM_PAINT
消息),为您希望立即显示的更改使用绘图。请注意,这些也应该在 WM_PAINT
处理中,因此您宁愿必须编写一些 drawing/painting 例程,将 HDC
(或 CDC*
)作为参数(以及需要的任何其他参数),并从 OnPaint()
函数(在此处传递 ClientDC
)和所需的其他绘图操作(传递通过调用 [=22= 获得的 CDC*
调用它们]).
所以,让我分享一下我很久以前写的一个应用程序的经验。它是一个 image-display/manipulation(除其他外)应用程序,以自定义格式处理图像,并使用一个特殊的库,而不是 "slow",因为它只提供了在设备上下文中显示图像的功能(这包括可能的裁剪、调整、调整大小等 CPU-costly 操作)。这是一张图片:
您可以看到用户正在执行选择。应用程序必须显示图像,可能还有图像顶部的选择矩形,当然这就是 OnPaint()
所做的。 "easy"(尽管技术上是 "correct")实现将调用 Invalidate()
或 InvalidateRect()
以响应每个鼠标移动消息(选择时)。这会导致完全重绘(即 "OK"),但也会遇到性能问题,因为 image-library 速度慢:如果您在无效后也调用 UpdateWindow()
(请求立即刷新)性能会很低(必须 reprocess/redisplay 图像),否则,刷新只会在一些(明显的)时间后发生。这是通过使用绘图(不是绘画)来响应 WM_MOUSEMOVE
消息来解决的:没有在那里失效,而是只绘制选择矩形(在恢复由先前的选择消息修改的部分之后 - 我只 backup/restore框架的四个边,而不是整个矩形)。结果,应用程序响应迅速且操作流畅,尽管库速度较慢,并且正确显示图像和选择,即使您切换到另一个应用程序然后再返回它,同时跟踪选择(虚线) .
关于您的实施的一些注意事项和建议(它有很多问题):
- 正如其他成员所指出的,您不能自己给
OnPaint()
打电话。特别是 Invalidate()
之后的那些调用完全没有意义。相反,如果您想要立即更新,请致电 UpdateWindow()
。
- Imo 在
OnPaint()
内执行计算是不正确的,我的意思是那些点计算(尽管在你的情况下计算是相当微不足道的)。 OnPaint()
应该只显示在您的代码的另一部分计算的数据。
- 此外,设置
m_cDiagram
文本并从 OnPaint()
中重新绘制也不行(可能会导致额外的绘制请求)。最好将这些移动到 OnBnClickedButtongo()
.
- 您不需要使整个客户区无效(特别是擦除)以导致重新绘制某些控件,而只需使这些控件无效。记住,
sleep_for()
函数是阻塞的,当你的循环是 运行. 时,WM_PAINT
消息不会被发送和处理。
- 顺便说一句,考虑一种 non-blocking 方法,例如使用计时器,正如@Barmak Shemirani 所建议的那样。或者,也可以由 运行 和 message-loop 自己编写一个 "non-blocing
sleep()
"(提取 CWinApp::Run()
中的部分代码并修改它)。
- 由于您有一个对话框并创建了单独的控件来显示您的数据,因此使用
OnPaint()
并不是一个好的实现,因为它会影响(绘制)整个客户区。它主要用于 类,如 CView
或 CScrollView
(或一般的 custom-painting CWnd
)。您在对话框的表面上绘制图形,并且必须执行计算以获得 m_cDiagram
中的坐标(顺便说一句,您可以使用 GetWindowRect()
然后 ScreenToClient()
代替)但最好使用一个 owner-drawn 控件(到 paint/draw 上的图形),这并不难,你只需要响应绘画请求(就像在 OnPaint()
中一样),以及你获得的设备上下文只能在控件上绘制,不能在对话框上绘制;坐标相对于控件的客户区,从(0,0)开始。
希望对您有所帮助
我正在尝试为自己编写一个小的 MFC 应用程序,以测试我正在训练的一些 AI。
所以我添加了一个图片控件和一个静态控件,我可以在主 Window 的 OnPaint() 方法中自由地绘制东西 Window。
当我的应用程序只绘制一次时它似乎工作正常,但我现在添加了一个在停止之前多次执行 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;
}
}
这是没有循环时的样子 运行:
这是循环 运行:
时的样子CWnd::OnPaint
is a response to WM_PAINT
消息,不应直接调用。
WM_PAINT
调用 CWnd::OnPaint
,后者调用 CPaintDC dc(this)
, which in turns calls BeginPaint
/EndPaint
API。这个消息+响应的顺序应该保持原样。
因此 CPaintDC dc(this)
必须在 OnPaint
中出现一次 - 并且只能出现一次 - 而不是在其他任何地方。覆盖 OnPaint
如下:
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()) {...}
条件。
要强制 window 重绘自身,请调用 Invalidate()
(same thing as InvalidateRect(NULL, TRUE)
)
InvalidateRect(NULL, TRUE)
请求重绘 window。系统将查看此请求,并在有机会时向 window 发送 WM_PAINT
消息。因此,对 InvalidateRect
的调用可能不会按照您期望它在顺序程序中工作的方式进行处理。例如,第二次连续调用 InvalidateRect
不会有任何效果。 Window 已标记为要更新。
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
和其他消息。
所以你需要一个额外的线程,或者干脆使用SetTimer
,并响应ON_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);
}
}
您似乎无法理解 invalidating/painting 的工作原理。 您应该首先阅读的文档是: Painting and Drawing
虽然许多开发人员建议仅在 WM_PAINT
处理(在 MFC 中为 OnPaint()
)中绘制,但这并不总是最佳解决方案,因为此消息是 low-priority,绘制可能不是立竿见影(有 "choppy" 的感觉),你可能会得到 "flickering" 的效果。
相反,我有时会推荐混合绘画:
- 在
WM_PAINT
处理中应用绘画。这应该绘制整个客户区(或者如果你想要更多 "optimized" 实现,则只绘制它的无效部分)。请注意,WM_PAINT
消息可能是由于 window 的移动、调整大小、取消隐藏等原因导致部分或全部客户区无效而收到的,除了以编程方式使它无效之外。因此,为了响应WM_PAINT
消息,您应该执行完全重绘,即您要显示的所有项目。 - 在应用程序繁忙时(不等待收到 "asynchronous"
WM_PAINT
消息),为您希望立即显示的更改使用绘图。请注意,这些也应该在WM_PAINT
处理中,因此您宁愿必须编写一些 drawing/painting 例程,将HDC
(或CDC*
)作为参数(以及需要的任何其他参数),并从OnPaint()
函数(在此处传递ClientDC
)和所需的其他绘图操作(传递通过调用 [=22= 获得的CDC*
调用它们]).
所以,让我分享一下我很久以前写的一个应用程序的经验。它是一个 image-display/manipulation(除其他外)应用程序,以自定义格式处理图像,并使用一个特殊的库,而不是 "slow",因为它只提供了在设备上下文中显示图像的功能(这包括可能的裁剪、调整、调整大小等 CPU-costly 操作)。这是一张图片:
您可以看到用户正在执行选择。应用程序必须显示图像,可能还有图像顶部的选择矩形,当然这就是 OnPaint()
所做的。 "easy"(尽管技术上是 "correct")实现将调用 Invalidate()
或 InvalidateRect()
以响应每个鼠标移动消息(选择时)。这会导致完全重绘(即 "OK"),但也会遇到性能问题,因为 image-library 速度慢:如果您在无效后也调用 UpdateWindow()
(请求立即刷新)性能会很低(必须 reprocess/redisplay 图像),否则,刷新只会在一些(明显的)时间后发生。这是通过使用绘图(不是绘画)来响应 WM_MOUSEMOVE
消息来解决的:没有在那里失效,而是只绘制选择矩形(在恢复由先前的选择消息修改的部分之后 - 我只 backup/restore框架的四个边,而不是整个矩形)。结果,应用程序响应迅速且操作流畅,尽管库速度较慢,并且正确显示图像和选择,即使您切换到另一个应用程序然后再返回它,同时跟踪选择(虚线) .
关于您的实施的一些注意事项和建议(它有很多问题):
- 正如其他成员所指出的,您不能自己给
OnPaint()
打电话。特别是Invalidate()
之后的那些调用完全没有意义。相反,如果您想要立即更新,请致电UpdateWindow()
。 - Imo 在
OnPaint()
内执行计算是不正确的,我的意思是那些点计算(尽管在你的情况下计算是相当微不足道的)。OnPaint()
应该只显示在您的代码的另一部分计算的数据。 - 此外,设置
m_cDiagram
文本并从OnPaint()
中重新绘制也不行(可能会导致额外的绘制请求)。最好将这些移动到OnBnClickedButtongo()
. - 您不需要使整个客户区无效(特别是擦除)以导致重新绘制某些控件,而只需使这些控件无效。记住,
sleep_for()
函数是阻塞的,当你的循环是 运行. 时, - 顺便说一句,考虑一种 non-blocking 方法,例如使用计时器,正如@Barmak Shemirani 所建议的那样。或者,也可以由 运行 和 message-loop 自己编写一个 "non-blocing
sleep()
"(提取CWinApp::Run()
中的部分代码并修改它)。 - 由于您有一个对话框并创建了单独的控件来显示您的数据,因此使用
OnPaint()
并不是一个好的实现,因为它会影响(绘制)整个客户区。它主要用于 类,如CView
或CScrollView
(或一般的 custom-paintingCWnd
)。您在对话框的表面上绘制图形,并且必须执行计算以获得m_cDiagram
中的坐标(顺便说一句,您可以使用GetWindowRect()
然后ScreenToClient()
代替)但最好使用一个 owner-drawn 控件(到 paint/draw 上的图形),这并不难,你只需要响应绘画请求(就像在OnPaint()
中一样),以及你获得的设备上下文只能在控件上绘制,不能在对话框上绘制;坐标相对于控件的客户区,从(0,0)开始。
WM_PAINT
消息不会被发送和处理。
希望对您有所帮助