与 Direct2D 图形线程通信





5.00/5 (7投票s)
我如何停止担忧并学会热爱 Direct2D。
引言
我写了一个 90 年代的屏幕保护程序!某种程度上。它使用一个图形线程在窗口上绘制矩形。该线程在一个循环中运行,并将每个矩形随机生成的坐标、大小和颜色信息存储在一个双端队列中。(事实证明,双端队列——或双端队列——是此简单动画图形应用程序的理想数据结构)。
https://www.youtube.com/watch?v=vHTySAlbICg
本项目使用了最新的 Windows Direct2D 图形用户界面。起初我发现很难上手,这让我有点恼火。我过去用过一些 GDI,觉得它已经够麻烦了。
在我开始在 Windows 10 上编程后,很明显 GDI 已经有些过时了,而时髦的人们则在使用 Direct2D。于是,我不情愿地开始谷歌搜索 Direct2D 并尝试我找到的示例项目。
说实话,起初我有点震惊。在我看来,Direct2D 会让我比 GDI 更烦躁。然而,我越使用 Direct2D,就越惊叹于它的强大功能以及它所能产生的丰富图形的美感。我是一个快乐的人。
背景
我曾经写过一篇关于“在工作线程中监控和控制递归函数”的文章。但那已经是很久以前的事了,我花了一段时间才重新跟上进度。
在我尝试了许多(未成功)的创建工作线程的半生不熟的方法后——我发现最好的方法受到了 Joseph M. Newcomer 2001 年关于“使用工作线程”文章的启发。
Newcomer 的文章中有太多信息,我无法声称已经全部理解或充分利用了它们(以及主题中其他大量信息)。我的代码很可能远不如它本可以达到的水平——所以如果你看到任何可以改进的地方,请务必告诉我!
使用代码
我将在这里介绍如何创建一个相对简单的程序,该程序能在鼠标单击时停止和启动 Direct2D 图形线程。在查看完这个更基础的项目代码后,您可能想下载我与本文一起提供的更高级项目(RectArt)的代码。
更高级的项目在第二个对话框上具有滑块和按钮,它们可以实时控制第一个对话框中动画的各个方面。
设置初始对话框
我创建了一个 MFC 对话框项目,并将其命名为“Colin”。我取消了粗边框、ActiveX 控件的选项,并删除了对话框默认放置的按钮和静态文本框。
然后我重写了 WM_RBUTTONDOWN
(鼠标右键按下)消息,并在其中加入了这些代码行...
void CColinDlg::OnRButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
StopThread();
SendMessage(WM_CLOSE);
CDialogEx::OnRButtonDown(nFlags, point);
}
好的,如果您实在需要,可以暂时注释掉尚未定义的 StopThread()
函数,然后编译运行(我不得不这样做),但不要花太长时间,因为我们还有很多工作要做……
在 .cpp 文件的顶部添加一个对 deque(或双端队列——发音为“deck”)的包含(它是一个通用类。抱歉,我对术语不太熟悉……我把它想象成一副扑克牌(考虑到我们要处理一个有序的矩形列表,这很贴切))。
我们还包含一行 '#pragma comment(lib, "d2d1")
',它指定了一个链接器选项(是什么我不清楚),这是 Direct2D 工作所必需的。
// ColinDlg.cpp : implementation file
//
#include "stdafx.h"
#include "Colin.h"
#include "ColinDlg.h"
#include "afxdialogex.h"
//This is for the container template we
// willl be using to store information about rectangles to be
// drawn on the screen
#include <deque>
//These are for Direct2D
#include <d2d1.h>
#pragma comment(lib, "d2d1")
在对话框的 .h 文件顶部附近,添加另一个对 deque 的包含(我们在这里也需要命名空间信息)和一些定义。有一个定义是我们程序允许绘制的最大矩形数量,另外三个是自定义消息定义,后面是一个我们将用于矩形的数据结构('PopRectStruct
'),它将是我们的 deque 使用的数据类型。
#include <deque>
#define MAXRECTS 1000
#define CM_GRAPHICSJOBLIST (WM_APP + 1)
#define CM_CLEARWINDOW (WM_APP + 2)
#define CM_DRAWRECT (WM_APP + 3)
struct PopRectStruct {
COLORREF colour;
float alpha = 1;
int sysID;
D2D1_RECT_F rect;
};
在对话框的 .h 文件中,为 Direct2D、线程和图形算法添加以下声明……
private:
//For Direct 2D
RECT m_rc;
HRESULT m_hr = S_OK;
ID2D1Factory* m_pDirect2dFactory;
ID2D1HwndRenderTarget* m_pRenderTarget;
ID2D1SolidColorBrush* m_pBrush;
//For the graphics thread
static UINT GraphicsThread(LPVOID pParam);
void GraphicsLoop();
void RectPopDeque(std::deque<PopRectStruct> &dq, D2D1_SIZE_F * rtSize);
这三个函数中的第一个是图形线程函数,您稍后会看到它非常简洁,它所做的只是调用 GraphicsLoop
函数,该函数唯一的职责是发送消息,这些消息会反复导致 OnCMGraphicsJobList
执行。我稍后会展示这一切是如何工作的。
现在,让我们在 .h 文件中的类大括号外部添加以下内存管理模板函数。
template<class Interface>
inline void SafeRelease(
Interface **ppInterfaceToRelease
)
{
if (*ppInterfaceToRelease != NULL)
{
(*ppInterfaceToRelease)->Release();
(*ppInterfaceToRelease) = NULL;
}
}
然后,让我们为同步对象和控件函数添加声明……
CEvent* m_pEventToggleThread;
CEvent* m_pEventToggleAnimation;
CEvent* m_pEventWindowCleared;
void StartThread();
void StopThread();
void ToggleThread();
在对话框的 .cpp 文件中,我们需要在构造函数中实例化 CEvent
对象,并确保在应用程序关闭时进行清理。
构造函数应编辑成如下所示:
CColinDlg::CColinDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(IDD_COLIN_DIALOG, pParent)
, m_pEventToggleThread(new CEvent(FALSE, TRUE))
, m_pEventToggleAnimation(new CEvent(FALSE, TRUE))
, m_pEventWindowCleared(new CEvent(FALSE, FALSE))
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
现在,创建一个 WM_CLOSE
消息的处理器,其中可以进行关联的删除操作,并将其编辑成如下所示……
void CColinDlg::OnClose()
{
// TODO: Add your message handler code here and/or call default
StopThread();
delete m_pEventToggleThread;
delete m_pEventToggleAnimation
delete m_pEventWindowCleared;
CDialogEx::OnClose();
}
那么,我们现在进展如何?我们有了线程和从线程调用的函数的声明。让我们为它们提供定义……
void CColinDlg::StartThread()
{
m_pEventToggleThread->SetEvent();
AfxBeginThread(GraphicsThread, (void *)this,
THREAD_PRIORITY_LOWEST);
}
void CColinDlg::StopThread()
{
while (WaitForSingleObject(m_pEventToggleAnimation->m_hObject, 0) ==
WAIT_OBJECT_0) {
//Suspend animation of the graphics loop
m_pEventToggleAnimation->ResetEvent();
} // keep checking until it is done
}
void CColinDlg::ToggleAnimation()
{
//If the animation is active
if (WaitForSingleObject(m_pEventToggleAnimation->m_hObject, 0) ==
WAIT_OBJECT_0) {
//Inhibit the random rectangles animation
m_pEventToggleAnimation->ResetEvent();
}
else
{
//Allow rectangles animation to resume
m_pEventToggleAnimation->SetEvent();
}
}
现在我们开始进入实质内容。是时候填充我们的 GraphicsThread
、GraphicsLoop
和 RectPopDeque
函数了。我之前已经描述了 GraphicsThread
和 GraphicsLoop
。RecPopDeque
是生成、存储和检索矩形数据的地方。
但首先还有几个声明。有一个数据类型,我在这里包含它主要是为了我自己的方便,而不是为了你的。ColourAlphaEnvelope
的一些成员仅在(有意义地)使用此程序的更高级版本(RectArt)中。我将其保留在此处,而不是替换为更简单的东西,以节省时间。
在对话框的 .h 文件中,将 ColourAlphaEnvelope
数据类型添加到全局作用域区域(类大括号外部的某个位置)。
//ColourAlphaEnvelope is a data structure for storing colour and alpha ranges
// The variable names ending in (W) are used in the more advanced
// code available to download with this article but will be
// left at set values here.
struct ColourAlphaEnvelope {
COLORREF colour = RGB(127, 127, 127);
int alpha = 170;
int alphaW = 127;
int RedW = 255;
int GreenW = 255;
int BlueW = 255;
};
然后将其作为私有成员变量添加到对话框类中……
ColourAlphaEnvelope m_colourEnvelope;
最后,我们添加线程函数、GraphicsLoop
和 RectPopDeque
的定义。
UINT CColinDlg::GraphicsThread(LPVOID pParam)
{
CColinDlg * self = (CColinDlg *)pParam;
self->GraphicsLoop();
return 0;
}
void CColinDlg::GraphicsLoop()
{
while (WaitForSingleObject(m_pEventToggleThread->m_hObject, 0) ==
WAIT_OBJECT_0)
{
SendMessageTimeout(this->m_hWnd, CM_GRAPHICSJOBLIST, 0, 0, 0, 0, 0);
}
}
void CColinDlg::RectPopDeque(std::deque<PopRectStruct>& dq, D2D1_SIZE_F * pRtSize)
{
PopRectStruct tPRS;
int width = static_cast<int>(pRtSize->width);
int height = static_cast<int>(pRtSize->height);
int x = rand() % width - 5.0f;
int y = rand() % height + 5.0f;
tPRS.rect = D2D1::RectF(
x - (rand() % 1160) / ((rand() % 90) + 1),
y - (rand() % 1160) / ((rand() % 90) + 1),
x + (rand() % 1160) / ((rand() % 90) + 1),
y + (rand() % 1160) / ((rand() % 90) + 1)
);
FLOAT alphaRangeLow = ((FLOAT)m_colourEnvelope.alpha/255 - (FLOAT)m_colourEnvelope.alphaW / (2*255));
FLOAT alphaRangeHigh = ((FLOAT)m_colourEnvelope.alpha/255 + (FLOAT)m_colourEnvelope.alphaW / (2 * 255));
int redRangeLow = GetRValue(m_colourEnvelope.colour) - m_colourEnvelope.RedW / 2;
int redRangeHigh = GetRValue(m_colourEnvelope.colour) + m_colourEnvelope.RedW / 2;
if (redRangeLow < 1) redRangeLow = 1;
if (redRangeHigh > 255) redRangeHigh = 255;
int greenRangeLow = GetGValue(m_colourEnvelope.colour) - m_colourEnvelope.GreenW / 2;
int greenRangeHigh = GetGValue(m_colourEnvelope.colour) + m_colourEnvelope.GreenW / 2;
if (greenRangeLow < 1) greenRangeLow = 1;
if (greenRangeHigh > 255) greenRangeHigh = 255;
int blueRangeLow = GetBValue(m_colourEnvelope.colour) - m_colourEnvelope.BlueW / 2;
int blueRangeHigh = GetBValue(m_colourEnvelope.colour) + m_colourEnvelope.BlueW / 2;
if (blueRangeLow < 1) blueRangeLow = 1;
if (blueRangeHigh > 255) blueRangeHigh = 255;
FLOAT alphaRange = alphaRangeHigh - alphaRangeLow;
int redRange = redRangeHigh - redRangeLow;
int greenRange = greenRangeHigh - greenRangeLow;
int blueRange = blueRangeHigh - blueRangeLow;
FLOAT randF1 = rand() % (1000);
FLOAT randF2 = rand() % (1000);
if (randF1 < randF2)
tPRS.alpha = randF1 / randF2;
else
tPRS.alpha = randF2 / randF1;
tPRS.alpha = alphaRangeLow + tPRS.alpha*alphaRange;
if (tPRS.alpha>1) tPRS.alpha = 1;
if (tPRS.alpha<0) tPRS.alpha = 0;
int red, green, blue;
if ((redRangeHigh - redRangeLow)>1)
red = redRangeLow + rand() % redRange;
else red = redRangeLow;
if ((greenRangeHigh - greenRangeLow)>1)
green = greenRangeLow + rand() % greenRange;
else green = greenRangeLow;
if ((blueRangeHigh - blueRangeLow)>1)
blue = blueRangeLow + rand() % blueRange;
else blue = blueRangeLow;
tPRS.colour = RGB((DWORD)red, (DWORD)green, (DWORD)blue);
if (dq.size() <= (std::size_t) MAXRECTS)
dq.push_back(tPRS);
else {
dq.back() = tPRS;
dq.pop_front();
}
}
现在没什么了。打开类向导(Ctrl-Shift-X),确保在右上角选中了对话框类。单击“消息”选项卡,然后单击底部的“添加自定义消息”按钮。将消息名称设置为 CM_GRAPHICSJOBLIST
,将处理程序名称设置为 OnCMGraphicsJobList
。双击处理程序列表中的相应处理程序名称,即可直接进入编辑,然后粘贴此处的功能内容,使其如下所示:
afx_msg LRESULT CColinDlg::OnCMGraphicsJobList(WPARAM wParam, LPARAM lParam)
{
m_hr = S_OK;
if (!m_pRenderTarget) {
GetClientRect(&m_rc);
D2D1_SIZE_U size = D2D1::SizeU(
m_rc.right - m_rc.left,
m_rc.bottom - m_rc.top
);
m_hr = m_pDirect2dFactory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(m_hWnd, size),
&m_pRenderTarget
);
}
if (m_pRenderTarget) {
m_pRenderTarget->BeginDraw();
SendMessageTimeout(this->m_hWnd, CM_CLEARWINDOW, 0, 0, 0, 0, 0);
SendMessageTimeout(this->m_hWnd, CM_DRAWRECT, 0, 0, 0, 0, 0);
m_pRenderTarget->EndDraw();
}
m_hr = S_OK;
SafeRelease(&m_pRenderTarget);
return 0;
}
我想这里需要注意的代码位置是 m_pRenderTarget
与 if (m_pRenderTarget)
块之前和之后,以及 BeginDraw
和 EndDraw
函数如何在 CM_CLEARWINDOW
和 CM_DRAWRECT
上调用 SendMessageTimeOut
之前和之后。
所以,这段代码完成了大部分 Direct2D 工作。我不会试图自己解释它。我发现它有点棘手,但幸运的是,现在它能正常工作了。当然,我建议读者使用他们喜欢的搜索引擎,并尽可能多地阅读相关内容。同样,如果我遗漏了什么或弄错了什么——请告诉我!
如果你愿意,可以将多个图形作业放在这个函数中(正如你下载并检查示例“RectArt”代码时会看到的)。这些单独的图形作业项可以被放入一些根据 CEvent 同步对象的状态而运行或不运行的块中——从而让你对图形线程的控制程度达到令人满意的水平。
您会注意到还有两个自定义消息 CM_CLEARWINDOW
和 CM_DRAWRECT
,它们的名称暗示了它们的作用。使用我上面描述的方法,使用类向导创建它们,并将它们分配给处理程序 OnCMClearWindow
和 OnCMDrawRect
。
我们在编写此项目早期添加了这些自定义消息的 #define
语句。如果您跳过了此阶段,请立即返回并完成。
快完成了!复制并粘贴我们刚刚创建的两个处理函数的内容……
afx_msg LRESULT CColinDlg::OnCMClearwindow(WPARAM wParam, LPARAM lParam)
{
if (m_pRenderTarget) {
m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::Black));
//Indicate to the OnCMGraphicsJobList loop function that the window
//has been cleared and that this function can be called again
m_pEventWindowCleared->SetEvent();
}
return 0;
}
afx_msg LRESULT CColinDlg::OnCMDrawrect(WPARAM wParam, LPARAM lParam)
{
static PopRectStruct RectanglesAry[MAXRECTS];
static std::deque<PopRectStruct> RectDq;
if (m_pRenderTarget) {
D2D1_SIZE_F * pRtSize = &(m_pRenderTarget->GetSize());
//Prevent crashing when window is minimised
if (pRtSize->height > 0 && pRtSize->width > 0) {
//Is rectangles animation currently switched on?
if (WaitForSingleObject(m_pEventToggleAnimation->m_hObject, 0) ==
WAIT_OBJECT_0) {
RectPopDeque(RectDq, pRtSize);
}
for (unsigned i = 0; i < RectDq.size() - 1; i++) {
m_pRenderTarget->CreateSolidColorBrush(
D2D1::ColorF((float)GetRValue(RectDq.at(i).colour) / 255, (float)GetGValue(RectDq.at(i).colour) / 255, (float)GetBValue(RectDq.at(i).colour) / 255,
RectDq.at(i).alpha),
&m_pBrush
);
m_pRenderTarget->FillRectangle(RectDq.at(i).rect, m_pBrush);
SafeRelease(&m_pBrush);
}
}
}
return 0;
}
如果您现在像我一样尝试编译并运行项目,期望它能工作,您会和我一样感到失望。这是我刚得到的结果:
Exception thrown: read access violation. this was nullptr.
我忘了创建 Direct2D 工厂。别问我 Direct2D 工厂是什么,但没有它这个项目将无法运行。还有其他一些初始化。复制这里的代码,使您的构造函数如下所示:
CColinDlg::CColinDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(IDD_COLIN_DIALOG, pParent)
, m_pEventToggleThread(new CEvent(FALSE, TRUE))
, m_pEventToggleAnimation(new CEvent(FALSE, TRUE))
, m_pEventWindowCleared(new CEvent(FALSE, FALSE))
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_pRenderTarget = NULL;
// Create a Direct2D factory.
m_hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);
m_pEventToggleAnimation->SetEvent();
m_pEventWindowCleared->SetEvent();
}
另外,您希望在运行程序时启动线程,所以在 OnInitDialog 函数中调用 StartThread()
。您也可以顺便调用 srand(time(NULL))
;——如果您将来多次运行完成的程序,这样可以给您的图形带来一些多样性。
BOOL CColinDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
srand(time(NULL));
StartThread();
return TRUE; // return TRUE unless you set the focus to a control
}
所以我们做了所有这些工作。您可能已经运行了代码。希望成功了。也许您在问,我们所做的可能有点令人失望?也许吧。但有很多方法可以使这个项目对您来说更有趣。
这是一个小小的进步
我们已经重写了鼠标右键单击以退出程序。让我们重写鼠标左键单击以暂停动画……
打开类向导,在“消息”选项卡下,重写 WM_LEFTBUTTONDOWN
消息,并在其中调用 ToggleAnimation()
。
我知道,我知道。不过,它可以变得更有趣。我保证。请继续阅读……
关注点
本文档中的代码(Colin)基于演示应用程序(RectArt)的代码。我最初希望写一篇描述 Rectart 的文章,但这会花费我太长时间,因为 RectArt 在一个单独的对话框窗口中向用户呈现了一个相当大的界面。
然而,本文档描述了 Rectart 中使用的相同图形线程和 Direct2D 用法。我有可能有一天,尤其是在得到鼓励的情况下,会写第二篇文章来描述 RectArt 中使用的控件对话框界面。
Rectart 中存在但 Colin 中不存在的功能
- 一个控件对话框界面,可以通过右键单击动画图形来切换开关。
- 全屏无边框图形,扩展到应用程序托盘上方。可以通过鼠标单击、按住和拖动来最小化和移动。例如,这对于在一个显示器上显示图形动画,而在另一个显示器上使用控件对话框非常方便。
- 颜色中心和范围滑块。
- 通过滑块调整矩形的最大数量。
- 通过滑块调整绘制矩形之间的时间延迟。
- 一个低频振荡器,用于调整矩形的最大数量,其频率可以通过滑块进行调整。
- 一个单独的图形任务,在屏幕上绘制一个网格(请参阅上面关于 On
CMGraphicsJobList
函数的部分)。 - 图形线程的反馈显示,显示当前队列中的矩形数量。
最后
向 Tim Hunkin 致敬,他有一个 animatronic 艺术评论家,您可以在 Southwold Pier 咨询他!
http://www.timhunkin.com/95_isitart.htm
历史
2016 年 6 月 27 日 - 第一版。