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

与 Direct2D 图形线程通信

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2016年6月27日

CPOL

10分钟阅读

viewsIcon

17916

downloadIcon

1676

我如何停止担忧并学会热爱 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();
    }
}

现在我们开始进入实质内容。是时候填充我们的 GraphicsThreadGraphicsLoopRectPopDeque 函数了。我之前已经描述了 GraphicsThreadGraphicsLoopRecPopDeque 是生成、存储和检索矩形数据的地方。

但首先还有几个声明。有一个数据类型,我在这里包含它主要是为了我自己的方便,而不是为了你的。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;

最后,我们添加线程函数、GraphicsLoopRectPopDeque 的定义。

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_pRenderTargetif (m_pRenderTarget) 块之前和之后,以及 BeginDrawEndDraw 函数如何在 CM_CLEARWINDOWCM_DRAWRECT 上调用 SendMessageTimeOut 之前和之后。

所以,这段代码完成了大部分 Direct2D 工作。我不会试图自己解释它。我发现它有点棘手,但幸运的是,现在它能正常工作了。当然,我建议读者使用他们喜欢的搜索引擎,并尽可能多地阅读相关内容。同样,如果我遗漏了什么或弄错了什么——请告诉我!

如果你愿意,可以将多个图形作业放在这个函数中(正如你下载并检查示例“RectArt”代码时会看到的)。这些单独的图形作业项可以被放入一些根据 CEvent 同步对象的状态而运行或不运行的块中——从而让你对图形线程的控制程度达到令人满意的水平。

您会注意到还有两个自定义消息 CM_CLEARWINDOWCM_DRAWRECT,它们的名称暗示了它们的作用。使用我上面描述的方法,使用类向导创建它们,并将它们分配给处理程序 OnCMClearWindowOnCMDrawRect

我们在编写此项目早期添加了这些自定义消息的 #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 中不存在的功能

  1. 一个控件对话框界面,可以通过右键单击动画图形来切换开关。
  2. 全屏无边框图形,扩展到应用程序托盘上方。可以通过鼠标单击、按住和拖动来最小化和移动。例如,这对于在一个显示器上显示图形动画,而在另一个显示器上使用控件对话框非常方便。
  3. 颜色中心和范围滑块。
  4. 通过滑块调整矩形的最大数量。
  5. 通过滑块调整绘制矩形之间的时间延迟。
  6. 一个低频振荡器,用于调整矩形的最大数量,其频率可以通过滑块进行调整。
  7. 一个单独的图形任务,在屏幕上绘制一个网格(请参阅上面关于 On CMGraphicsJobList 函数的部分)。
  8. 图形线程的反馈显示,显示当前队列中的矩形数量。

最后

向 Tim Hunkin 致敬,他有一个 animatronic 艺术评论家,您可以在 Southwold Pier 咨询他!

http://www.timhunkin.com/95_isitart.htm

历史

2016 年 6 月 27 日 - 第一版。

© . All rights reserved.