Introduction
I have written a 1990s screen saver! Sort of. It uses a graphics thread to draw rectangles on a window. The thread runs in a loop and stores randomly generated position, size and colour information about each rectangle in a deque. (A deque - or double ended queue - turns out to be the ideal data structure for use in this simple animated graphics application).
https://www.youtube.com/watch?v=vHTySAlbICg
This project uses the recent Windows Direct2D graphics user interface. I found it hard to get to grips with at first and the whole thing made me a bit grumpy. I'd used a bit of GDI in the past and found that to be awkward enough.
After I started programming on Windows 10 it became apparent that GDI was somewhat old hat and the cool kids were using Direct2D instead. So, huffily, I started Googling Direct2d and trying out the example projects that I found.
To be honest, at first, I was a bit horrified. It seemed to me that Direct2D was going to make me even grumpier than GDI had. However, the more I use Direct2D the more amazed I am at its power and the beauty of the rich graphics that it can produce. I am a happy man.
Background
I did an article a while back on Monitoring and Controlling a Recursing Function in a Worker Thread. This was so long ago, though, it took me a while to get back up to speed.
After I tried numerous half-cocked recipes for making a thread work (that didn't) - the best one I have come up was inspired by Joseph M. Newcomer's article from back in 2001 on Using Worker Threads.
Newcomer has so much information in his articles I can't pretend to have taken it all in or made best use of it (or of the multitudinous information there is elsewhere on the topic). It is highly likely that my code isn't nearly as good as it could be - so if you see some way I could improve it then please, please let me know!
Using the code
I am going to describe here how to create a relatively simple program that stops and starts a Direct2D graphics thread on a mouse click. Once you have looked through the code for this more basic project you might want to download the code for the more advanced project (RectArt) which I am including with this article.
The more advanced project has sliders and buttons on a second dialog which control various aspects of the animation on the first dialog in real time.
Setting up the initial dialog
I have made an MFC dialog project and called it 'Colin'. I de-selected the options for a heavy border, ActiveX controls and removed the buttons and static text box that are placed in the dialog by default.
Then I overrode the WM_RBUTTONDOWN
down message placing these lines of code in it...
void CColinDlg::OnRButtonDown(UINT nFlags, CPoint point)
{
StopThread();
SendMessage(WM_CLOSE);
CDialogEx::OnRButtonDown(nFlags, point);
}
Ok, you can temporarily rem out the as-yet undefined function StopThread()
, compile and run it if you really have to (I had to) but don't be long because we've got a lot to do here....
In at the top of the .cpp file add an include for a deque (or double ended queue - pronounce 'deck'), this is a general data class (Forgive me, I'm not too good on the terminology here... I think of it rather like a deck of cards (quite appropriate seeing as we will be dealing with an ordered list of rectangles).
We alse include a line '#pragma comment(lib, "d2d1")'
which specifies a linker option (what ever that is) that is required for Direct2D to work.
#include "stdafx.h"
#include "Colin.h"
#include "ColinDlg.h"
#include "afxdialogex.h"
#include <deque>
#include <d2d1.h>
#pragma comment(lib, "d2d1")
Near the top of the dialog's .h file add another include for deque (we need the namespace information here, too) and some defines. There is one define for the maximum number of rectangles we are going to allow our program to draw and three more custom message defines followed by a data structure ('PopRectStruct'
) which we are going to use for our rectangles and, hence, is going to be the data type used by our 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;
};
In the dialog's .h file add the following declarations for Direct2D, the thread and the graphics alogorithm....
private:
RECT m_rc;
HRESULT m_hr = S_OK;
ID2D1Factory* m_pDirect2dFactory;
ID2D1HwndRenderTarget* m_pRenderTarget;
ID2D1SolidColorBrush* m_pBrush;
static UINT GraphicsThread(LPVOID pParam);
void GraphicsLoop();
void RectPopDeque(std::deque<PopRectStruct> &dq, D2D1_SIZE_F * rtSize);
The first of these three functions is the graphics thread function and you will see later that this is very minimalistic and all that it does is call the GraphicsLoop
function which has sole job of pumping out messages that repeatedly cause OnCMGraphicsJobList
to execute. I'll show you how this all works a little later.
For now let's add the following memory management template function to the .h file somewhere outside of the class braces.
template<class Interface>
inline void SafeRelease(
Interface **ppInterfaceToRelease
)
{
if (*ppInterfaceToRelease != NULL)
{
(*ppInterfaceToRelease)->Release();
(*ppInterfaceToRelease) = NULL;
}
}
Then in let's add declarations for the synchronisation objects and control functions...
CEvent* m_pEventToggleThread;
CEvent* m_pEventToggleAnimation;
CEvent* m_pEventWindowCleared;
void StartThread();
void StopThread();
void ToggleThread();
In the dialog's .cpp file we need to instantiate the CEvent
objects in the constructor and make sure they are tidied away when the application is closed down:
The constuctor should be edited so that it looks like this:-
CColinDlg::CColinDlg(CWnd* pParent )
: 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);
}
Now a handler for the WM_CLOSE
message should be made where the associated deletes can be made and edited so that it looks like this...
void CColinDlg::OnClose()
{
StopThread();
delete m_pEventToggleThread;
delete m_pEventToggleAnimation
delete m_pEventWindowCleared;
CDialogEx::OnClose();
}
So, where are we? We've got declarations for the thread and the functions which are called from it. Let's put in definitions for those....
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) {
m_pEventToggleAnimation->ResetEvent();
}
}
void CColinDlg::ToggleAnimation()
{
if (WaitForSingleObject(m_pEventToggleAnimation->m_hObject, 0) ==
WAIT_OBJECT_0) {
m_pEventToggleAnimation->ResetEvent();
}
else
{
m_pEventToggleAnimation->SetEvent();
}
}
Now we're getting down to the nitty gritty. It's time to populate our GraphicsThread
, GraphicsLoop
and RectPopDeque
functions. I described GraphicsThread
and GraphicsLoop
earlier. RecPopDeque
is where the data for the rectangles is generate, stored and retrieved from the deque.
But first there are a couple more declarations. There is a data type which I am including here more for my own convenience than for yours. Some of the members of ColourAlphaEnvelope
are only used (in a meaningfull way) in the more advanced version of this program (RectArt). I'm leaving it in here rather than substitute it for something more straightforward to save myself some time.
In the dialog's .h file add ColourAlphaEnvelope
data type in the global scope area (somewhere outside of the class braces).
struct ColourAlphaEnvelope {
COLORREF colour = RGB(127, 127, 127);
int alpha = 170;
int alphaW = 127;
int RedW = 255;
int GreenW = 255;
int BlueW = 255;
};
and then add this as a private member variable to the dialog class....
ColourAlphaEnvelope m_colourEnvelope;
At last we add the definitions for the thread function, GraphicsLoop
and 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();
}
}
Not much to do now. Open up the Class Wizard (Control-Shift-X), make sure you have the dialog class selected in the top right hand corner. Click on the 'Messages' tab and then on the 'Add Custom Message' button at the bottom. Set the message name as CM_GRAPHICSJOBLIST
and the handler name as OnCMGraphicsJobList
. Double click on the resulting handler name in the list of handlers to take you straight into editing it and paste in the function contents here so you get this:
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;
}
I guess the thing to pay attention to here is the positioning of the code relating to m_pRenderTarget
before and after the if (m_pRenderTarget)
block and how the BeginDraw
and EndDraw
functions are called before and after the SendMessageTimeOut
s on CM_CLEARWINDOW
and on CM_DRAWRECT
.
So, this code does the bulk of the Direct2D work. I won't try and explain it myself. I found it quite tricky to get to work but, thankfully, now it does. I would, of course, recommend the reader use their favourite search engine and read up all they can on the subject. And again, if there is anything I have missed or got wrong - please let me know!
You could, if you wanted to, place a number of graphics jobs in this function (as you will see if you download and inspect the sample 'RectArt' code). These individual graphics job items can then be put in blocks that run or not depending on the state of CEvent synchronisation objects - thereby giving you a satisfying amount of control over what you can do with your graphics thread.
You will have noticed that there are two more custom messages CM_CLEARWINDOW
and CM_DRAWRECT
whose names hint at what they cause to happen. Use the method I describe above to create them with the Class Wizard and assign them the handlers OnCMClearWindow
and OnCMDrawRect
respectively.
We added #define
statements for these custom messages early on in writing this project. If you skipped this stage go back and do it now.
Nearly there! Copy and paste in the contents of the two handler functions we just made...
afx_msg LRESULT CColinDlg::OnCMClearwindow(WPARAM wParam, LPARAM lParam)
{
if (m_pRenderTarget) {
m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::Black));
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());
if (pRtSize->height > 0 && pRtSize->width > 0) {
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;
}
If you now do what I just did and compile and run the project expecting it to work you will be, like I was, disappointed. This is what I just got:
Exception thrown: read access violation.
this was nullptr.
I forgot to create the Direct2D factory. Don't ask me what a Direct2D factory is but this project isn't going to run without one. There are couple of other initialisations. Copy the code here to make your constuctor look like this:-
CColinDlg::CColinDlg(CWnd* pParent )
: 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;
m_hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);
m_pEventToggleAnimation->SetEvent();
m_pEventWindowCleared->SetEvent();
}
Also, you will want your thread to start off when you run your program so put a call to StartThread()
in your OnInitDialog function. You could pop in a call to srand(time(NULL))
; while you are at it - to give your graphics some variety if you should ever run the finished program more than once.
BOOL CColinDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE);
srand(time(NULL));
StartThread();
return TRUE; }
So we've done all of this work. You will have run the code. Hopefully successfully. Perhaps you are asking yourself if may be it is just a little bit disappointing what we've done here? Well perhaps. But there are so many ways you could make this project more interesting for yourself.
Here's a small step forward:
We have over ridden the right-hand mouse click to exit the program. Let's override the left-hand mouse click to pause the animation....
Open up the Class Wizard and under the 'messages' tab override the WM_LEFTBUTTONDOWN
message and put a call to ToggleAnimation()
in it.
I know, I know. It can be made more interesting, though. I promise. Read on...
Points of Interest
The code in this article (Colin) is based on the code for demo application (RectArt). I was originally hoping to write an article describing Rectart but it would have taken me too long because RectArt has quite a substanctial interface presented to the user in a seperate dialog window.
This article, though, describes the same graphics thread and use of Direct2D that are in Rectart. It is just possible that I might, one day, especially if I get any encouragent write a second article describing the control dialog interface used in RectArt.
Features in Rectart not present in Colin:
- A control dialog interface that can be toggled on or off by right-clicking on the animating graphics
- Full screen borderless graphics that extend over the application tray. This can be minimised and moved by clicking, holding and dragging with the mouse. This is handy, for instance, for having the graphics animation displaying on one monitor whilst using the control dialog on another monitor.
- Colour centre and range sliders.
- Adjust the maximum number of rectangles with a slider.
- Adjust the delay between drawing rectangles with a slider.
- A low frequency oscillator on the maximum number of rectangles the frequency of which can be adjusted with a slider.
- A separate graphics task which draws a grid on the screen (see the section above on On
CMGraphicsJobList
function).
- Feedback display from the graphics thread showing the number of rectangles currently in the deque.
And finally:
A shout out to Tim Hunkin who has an animatronic art critic who you can consult on Southwold Pier!
http://www.timhunkin.com/95_isitart.htm
History
27th June, 2016 - First edition.