Introduction
Have you ever needed high-quality graphics in your Win32 application, with support for transparency (alpha channel) or anti-aliasing? Have you ever solved the dilemma whether to use old and relatively slow GDI+ or rather much newer Direct2D but at the cost that your application won't support older systems? If your answer to both of these questions is "yes", then you may find this article (and the code it presents) useful.
Sometime ago, I was solving exactly the dilemma outlined above, when I was working on the chart control for the mCtrl project. My eventual answer was creation of a light-weight wrapper code which wraps both under uniform and easy-to-use API. Over time, the code grew, as I needed more and more from it. And finally it has reached the point where it is useful on its own and hence I decided to isolate that code into a standalone and reusable package.
Yesterday, the baby was finally uploaded to GitHub. For the lack of my inspiration, it was given very uncreative name, WinDrawLib. So say hello to it and let me to introduce it here, to the audience which, as I hope, may find it useful.
Its main goal is to allow using Direct2D on newer machines and but still allow application to function as well as possible on older systems where Direct2D is not available, most notably Windows XP and Vista (without ServicePack), which is achieved by falling back to GDI+ usage.
An example what WinDrawLib can do with just a few lines of code.
Design
As said, the purpose of the library is to offer uniform interface for work with GDI+ and Direct2D (and some related libraries), while being as light-weight as possible and having an interface which is easy to understand and adopt.
To achieve that, the code must deal with many differences between GDI+ and Direct2D. The most notable is that each of them uses completely different types. Fortunately, interface of both libraries is in many ways similar conceptually and so, when hidden behind some opaque handle types, application does not need to know whether behind a handle WD_HBRUSH
is GDI+'s GpBrush
or Direct2D's ID2D1Brush
.
Another obstacle lays in the fact that GDI+ provides C++ interface which is more or less unsuitable for playing with LoadLibrary()
and GetProcAddress()
. This was solved by using the underlying C "flat" interface.
Last but not least, to keep the API simple, I decided to ignore some features for the sake of simplicity. For example there is no explicit notion of a pixel format in the API.
In short, WinDrawLib is meant mainly as a "better GDI with alpha channel and anti-aliasing", not a general purpose and powerful graphics library.
System Requirements
The library should run on any Windows version which has GDIPLUS.DLL available or newer. This should cover some Windows 2000, and anything newer.
AFAIK, original vanilla Windows 2000 did not provide GDIPLUS.DLL but it may be present with some service pack or system update. Furthermore, there is also some old redistributable version of GDIPLUS.DLL available from Microsoft which can help you if still need to support all Windows 2000 machines :-)
By default, the library uses Direct2D if it is available on the system, and if it is not, it falls back to the GDI+.
Current Status
The API is in no way complete. Both GDI+ and Direct2D provide much more features and options then what's exposed through WinDrawLib. Some were omitted by intention as noted above. Many more could be added in the future but so far I simply didn't need them.
Anyway, I believe the library may already be useful as it is. Furthermore, if you see it as a useful tool you lack some feature, you may consider to implement it on your own and share it with me and others and make it better for every one.
Also note I do not consider the API rock-stable: There may be some changes in names or count of parameters here or there, but likely nothing big and nothing what would change it conceptually, and as I hope the API should converge to something more stable as more eyes take a look on it.
As already said above, the library is living on GitHub, it is available under quite permissive open source license (MIT) and as such you are free to modify the code under its terms. Any useful additions respecting the spirit of the code are welcome. So feel free to make GitHub's pull requests or provide them as raw patches if you are unfamiliar with git or GitHub.
Feel free to also contribute with enhancements to documentation (which is currently only in the form of sparse comments in the public header wdl.h) or provide new examples demonstrating the use of the API: Few already live in the repo as well.
No Release Cycle
Please do not expect any standard release cycle or regular binary release packages from me. I feel the code is quite simple and small, it is of an auxiliary nature, I plan to maintain the master branch of the repository to be as stable as possible, but it's up to you to build the library whenever you need it from the sources.
To do so, you have to use CMake or to manually create some Makefile or Project File. Given the library has more-or-less no hard dependencies (it loads everything in run time via LoadLibrary()
as it is not known at advance what library shall be needed or what is available on the system), it should not be a problem.
Usage
There is likely no sense in explaining every WinDrawLib's function for painting of a basic primitive. So let me just list the currently provided functions. They have mostly self-describing names so it can give you an idea what the library can do.
Later in the article we will take a look how to use some parts of the API which may not be so obvious at the first sight.
After all, most functions are used in very straightforward way with no need to explain meaning of their parameters, there is also some documentation in the public header, wdl.h, which exposes all WinDrawLib public types and functions.
So to use WinDrawLib, just include this header and link with the static library, WINDRAWLIB.LIB. (Assuming you've built it with Visual Studio as other compilers may use different naming conventions.)
Overview of API
Helper types:
WD_COLOR
WD_CIRCLE
WD_LINE
WD_POINT
WD_RECT
Opaque handle types:
WD_HCANVAS
: Any object which can be painted on. WD_HBRUSH
: A virtual brush to paint with. WD_HFONT
: Font for text output. WD_HIMAGE
: Any image-like object. WD_HPATH
: Path objects represent complex and reusable shapes.
Initialization:
wdPreInitialize()
wdInitialize()
wdTerminate()
Canvas management:
wdCreateCanvasWithPaintStruct()
wdCreateCanvasWithHDC()
wdDestroyCanvas()
wdBeginPaint()
wdEndPaint()
wdResizeCanvas()
wdStartGdi()
wdEndGdi()
wdClear()
wdSetClip()
wdRotateWorld()
wdTranslateWorld()
wdResetWorld()
Brush management:
wdCreateSolidBrush()
wdDestroyBrush()
wdSetSolidBrushColor()
Font management:
wdCreateFont()
wdCreateFontWithGdiHandle()
wdDestroyFont()
wdFontMetrics()
Image management:
wdCreateImageFromHBITMAP()
wdLoadImageFromFile()
wdLoadImageFromIStream()
wdLoadImageFromResource()
wdDestroyImage()
wdGetImageSize()
Path management:
wdCreatePath()
wdCreatePolygonPath()
wdDestroyPath()
wdOpenPathSink()
wdClosePathSink()
wdBeginFigure()
wdEndFigure()
wdAddLine()
wdAddArc()
Draw operations:
wdDrawArc()
wdDrawLine()
wdDrawPath()
wdDrawPie()
wdDrawRect()
Fill operations:
wdFillCircle()
wdFillPath()
wdFillPie()
wdFillRect()
Bit-blit operations:
wdBitBltImage()
wdBitBltHICON()
String output:
wdDrawString()
wdMeasureString()
wdStringWidth()
About Initialization
As an optional step before calling any other WinDrawLib function, you may call a function wdPreInitialize()
. This function serves two purposes:
- It allows to provide a pointer to
CRITICAL_SECTION
. If it isn't NULL
, WinDrawLib uses it to synchronize access to some internal global variables and hence allows concurrent use from multiple threads. - Additionally, it allows to disable Direct2D or GDI+ backend which may be useful for debugging purposes. E.g. to force the library to use the GDI+ back-end on new system where Direct2D is available, you may use the flag
WD_DISABLE_D2D
.
Note wdPreInitialize()
may only be called once and that the critical section, if provided, has to be already initialized and stay initialized as long as WinDrawLib is in use.
Main initialization is then performed by wdInitialize()
. This function is paired with wdTerminate()
which frees all resources taken by wdInitialize()
.
wdInitialize()
is responsible for loading D2D1.DLL or GDIPLUS.DLL. It may also load other libraries if you explicitly ask with parameter dwFlags for some features that require it:
Flag | Newer Windows
(Direct2D available) | Older Windows
(GDI+) |
WD_INIT_COREAPI | D2D1.DLL | GDIPLUS.DLL |
WD_INIT_IMAGEAPI | WINDOWSCODECS.DLL |
WD_INIT_STRINGAPI | DWRITE.DLL |
Notes:
- The core part of the library is always initialized as other parts depend on it.
- Refer to the comments in wdl.h which sets of functions require which initialization flag for their proper functioning.
This approach allows application to instruct WinDrawLib to load just libraries which are really needed.
wdInitialize()
may be called as many times as you want: It manages internal initialization counters for each of the modules. Respective wdTerminate()
decrements the counter and the module is really uninitialized if the counter reaches zero.
Painting on Window
When WinDrawLib is initialized, we need a canvas object we can paint on.
When painting to a window (HWND
) in a context of WM_PAINT
handler, you may create the canvas with the function wdCreateCanvasWithPaintStruct()
.
When painting outside of WM_PAINT
, there is the function wdCreateCanvasWithHDC()
which can create canvas for any device context (HDC
).
The painting code itself then has to be enclosed between calls wdBeginPaint()
and wdEndPaint()
. Simply create brushes or other objects, call functions drawing various primitives etc.
So, WM_PAINT
handler may look like this:
LRESULT CALLBACK
WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
case WM_PAINT:
{
PAINTSTRUCT ps;
WD_HCANVAS hCanvas;
WD_HBRUSH hBrush;
WD_RECT rc = { 10.0f, 10.0f, 50.0f, 50.0f };
BeginPaint(hwndMain, &ps);
hCanvas = wdCreateCanvasWithPaintStruct(hwndMain, &ps, 0);
wdClear(hCanvas, WD_RGB(255,255,255));
hBrush = wdCreateSolidBrush(hCanvas, WD_RGB(0,0,0));
wdDrawRect(hCanvas, hBrush, &rc, 5.0f);
wdDestroyBrush(hBrush);
wdDestroyCanvas(hCanvas);
EndPaint(hwndMain, &ps);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
Note some object like canvas or brush in the previous example, can be cached for reuse in subsequent WM_PAINT
messages, if all the following conditions are met:
- The canvas was created with
wdCreateCanvasWithPaintStruct()
. - Return valuer of
EndPaint()
is TRUE
. - Application recreates the object whenever it gets message
WM_DISPLAYCHANGE
.
However let me to point you to the example which presents the technique, to keep this article in palatable length.
About Paths
Path, representing with the opaque handle WD_HPATH
, represents a complex shape made of figures. Each figure is a consecutive sequence of segments, typically lines or arcs. Each figure may be open or closed.
The path can be used used for complex clipping of a canvas or for drawing or filling of the defined complex shape on the canvas.
However, creation of the path is a bit cumbersome, in order to hide differences between Direct2D and GDI+, so it deserves a short explanation. The process of creation is as follows:
- Create the path handle:
WD_HPATH hPath = wdCreatePath(hCanvas);
- Open a sink:
WD_PATHSINK pathSink;
wdOpenPathSink(&pathSink, hPath);
- Add as many figures as you like, each composing of a set of line and/or arc segments:
WD_POINT ptStart = { x, y };
wdBeginFigure(&pathSink, &ptStart);
wdAddLine(...);
wdAddArc(...);
wdEndFigure(&pathSink, TRUE); wdClosePathSink(&pathSink);
There is also a convenient wrapper wdCreatePolygonPath()
which does all of the above for you, if you are limited to a path which consists of a single closed figure made of only line segments (i.e. polygon).
Once the path object is initialized as above, you can easily call wdSetClip()
, wdDrawPath()
or wdFillPath()
to take use of it.
Finally, when not needed anymore, call wdDestroyPath()
to release the path object.
Conclusion
Well, I know the article itself is not that much interesting. But I have some hope the library presented in the article, despite its immature state, can have some attraction, at least for developers solving the same issue I did.
Document History
(Only non-cosmetic changes to the document shall be tracked here.)
- 2017-09-26: Changed license from LGPL to more permisive MIT.
- 2016-04-11: Document version 1.0.