Writing Win32 Apps with C++: V2 - part 1






4.70/5 (32 votes)
An independent framework to handle Win32 objects inside C++ classes.
Contents
- Contents
- Introduction
- Where to start from
- Handling Windows messages
- Handling Windows handlers
- Handling DCs
- Other helpers
- Hallo world
- Conclusions
Introduction
This is the fourth edition of this article, with a review code, based on the templates presented in "A policy based reference counting", providing classes to make "smart" Win32 objects.
I'll not go in to the introductory disclaimers of the previous article, they'll be the same here. Pure C++ and STL (and –obviously- "windows.h") this time.
Code structure
Same as in the previous article: a collection of headers whose dependency are solved in the headers themselves. The winx project is just a collection for those headers, with the GE_::winx
namespace containing Windows related classes and functions.
Where to start from
Let me be original: from the end.
int main() { STRACE(trc, 0, ("w0 main\n")); stdx::New<Win>()->set_createhook(); /*forget this, by now*/ return app::msgloop(CreateWindow(app::wndcls::frame(), "Hallo Window!", WS_VISIBLE|WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, 0,0, GetModuleHandle(0), 0)).run(); }
First thing: I used main
instead of WinMain
. But don't be fooled by this: the linker had been instructed to use the WINDOWS subsystem and to use mainCRTStartup
as the entry point. We lose some useless Win16-only meaningful parameters, but we gain the C++ standard command line parsing (not needed here, but who knows!).
Forget about the stdx::New<Win>->set_createhook
and you find the entire program in the return
statement. (That is "we leave to die": although it seems pessimistic, if you are not in hurry ... you can do anything to do that, including live for one-hundred years or more!)
Now, app::msgloop
is a constructor taking an HWND
parameter. And run
is the usual Windows application message loop (while GetMessage(.) { Translate and Dispatch }
).
The HWND
is obtained with the Win32 CreateWindow
API (but anything creating a window works!), where the classname
parameter is obtained with the app:wndcls::frame
static function (that registers a WNDCLASSEX
) and GetModuleHandle(0)
is used to retrieve the HINSTANCE
for the executable module.
The run
function will return when IsWindow
for the passed HWND
will be found false
.
Well ... this is not perfectly orthodox in Windows (no PostQuitMessage
call to make GetMessage
to return 0) but works always, even if the main window is closed by an application unhandled syscommand, with no WM_QUIT
message to be posted.
Window class registration
The key is to box the RegisterWinClassEx
in a shortcutting class.
Class winx::wndcls
is a box class containing an stdx::refcountable
derived data structure to hold an ATOM
and HINSTANCE
values. wndcls
constructors keep some convenient default parameters to fill-in a WNDCLASSEX
structure, allocate a boxed structure referring it with a smart pointer, and call RegisterClassEx
storing the return value.
The on_delete
function of the internal structure calls UnregisterClass
when no more references exist. Thus, app::wndclass(…)
registers a Win32 window class on construction and unregisters it on the last destruction (copy and assignment pass the smart pointer, hence, sharing the ownership).
Since certain parameter value combinations are of frequent use, some static functions are available to shortcut such constructions: pane()
registers a "genericpane
" with IDI_APPLICATION
icon and IDC_CURSOR
and a COLOR_WINDOW
background; gray()
registers "genericgray
" with COLOR_BTNFACE
background, and frame()
registers a frame with no background.
All of them take DefWindowProc
as a window procedure. That makes all windows created with those classes to behave as default-behaving windows.
The message loop
Class app::msgloop
is constructed on the stack starting from an HWND
and an optional HACCEL
.
Its loopbody
function does the typical Get/Translate/Dispatch actions of a message loop body. It can be used as-is (for example, in calculation modal loops) or called by loop_while
(that loops until a given bool
reference becomes false
, or until the related HWND
exists) and run (that calls loop_while
passing an always true
variable).
Thus, the app::msgloop(...).run()
will create and run the loop until the passed HWND
(the newly created window) is destroyed.
Handling Windows messages
To stay with the Win32 API, Windows message handling happens through window procedures, associated to window classes, or to other "subprocedures" (like dialog procedure etc.) called by a system-defined default procedure. Handling messages for already existent windows can instead be done with subclassing (that is, replacing the window procedure with another).
This second method is more general, since it applies to every possible existing window. We can so create windows using the default procedure (DefWindowProc
) and then subclass them as needed.
But there are two problems to solve:
- A window procedure is a global-linkage function (a global or static member), while we would like to get messages into C++ class instances.
- A modal window exists inside a single API call (think of
DialogBox
: a window exists while it runs, but doesn't before and after the call). When do you subclass it?
Thunking, delegates and whatsoever
Associating an HWND
to an object instance and calling a member given the HWND
value can be done with a technique named "thunking". It's the technique used in ATL / WTL. In fact, it is a hack: a data structure (something residing in the data segment) is executed after appropriate CPU registers loading as its bytes are the machine code to push a this
pointer in the stack and do a jump.
This technique is generalized in the Don Clugston article on delegates.
Here I'll follow a different approach: HWND
to this
mapping is done … in a statically available std::map
for a static wndproc
that seizes the map and calls the instance on_message
function.
The subclasser
Class winx::win_subclasser
does exactly that, through the subclass
and on_message
function (and the subclass opposite reclass
). And since it is a template taking a Derived
class, we will have as many map and window procedures as C++ classes. And hence, each map will have as many entries as the total number of windows subclassed by the same C++ class. Hence, with an accurate definition of classes, seeking in the map can be not so time wasting.
As a consequence, we can sublclass the same HWND
using different C++ classes and handle different HWND
with different instances of a given C++ class. But not subclass twice an HWND
with two instances of the same Derived
. (And –in fact- it should be quite strange: why subclass twice to do the same things twice?)
The on_message
function is what you'll override in your implementation classes. The base implementation (remember to call it!) handles the map HWND
erasure at WM_NCDESTROY
. The from_hwnd
static function, seeks for the subclasser of a given type, creating it if not available.
Since win_subclasser
is an stdx::refcountable
and since the HWND
map holds strong pointers, a subclass will exist until someone refers it, or until its referred HWND
is destroyed.
Creation hook
The problem of handling the creation messages or handling modal windows' messages can be managed with the createhook
class (that's also the base for win_subclasser
): before creating the HWND
(with any Windows API) just instantiate the appropriate createhook
derived, and call set_createhook()
. A Windows wndproc
hook is installed, and the first WM_NCCREATE
message seen will map the HWND
with the C++ class instance. Then, the hook is deinstalled, and all subsequent messages received by the C++ class instance. The WM_NCDESTROY
will cause the reclass
thus eliminating the mapping.
Message routing
Receiving messages in a C++ class can be only a first step, in a complex application. When there is the need to "route" messages between objects (think of frames, views, documents and items) winmsgrouter
/ winmsglink
classes can be a solution. They are respectively the nodes and the arcs of a generic graph. Nodes have virtual bool on_message
and bool route_message
functions, all taking a winmsg
structure (a convenient wrapper for window procedure parameters and message routing information).
Routing functionality is –in fact- a specialization of the generics graph::link
, router
and routabledata
templates, that, in a Windows related context become winx::winmsglink
, winmsgrouter
and winmsg
, respectively.
Calling routemessage
pushes in the parameter structure the arcs begin/end iterators, and calls the do_default
function that iterates the arcs' destinations, calling on_message
until someone will return true.
Of course, you can override winmsgrouter::on_message
calling routemessage
once again, propagating the message through the graph.
Arcs contain a property named metric
, (accessible with get_
and set_
), whose value is added as the message is routed. The idea is let on_message
to have a way to know about how far a message comes from. By discarding too far-coming messages, you can have a very simple way to avoid routing loops.
Dynamic windows
An alternative way to distribute messages is the win
class. It combines a winmsgrouter
with a createhook
. An internally defined source
combines a win_subclasser
with a winmsgrouter
having on_message
(that's called by the subclasser window procedure) calling routemessage
. The createhook::set_hwnd
is overridden to create a winmsglink
between the win
and the HWND
associated source
.
win
also contains a self-pointing strong pointer that is initialized when the HWND
is assigned to it, and cleared when WM_NCDESTROY
is received or when the HWND
is de-assigned. This makes the class to exist until some other strong pointer holds it or until its referred HWND
exists.
Now, let's go back to the "/* forgot this by now */
" of the previous code fragment: Win
is a winx::win
derived class defined in the test unit: instantiating one and calling set_createhook
makes the window created soon after to be subclassed by a winx::win::source
and its messages to be routed to Win
. Since nothing else in the program will refer to Win
, it will be deleted at HWND
destruction (when the user will close the window).
To recap, the following picture shows the Windows and messages related classes.
Handling Windows handlers
Windows handlers can be reference counted with value-semantic wrappers. In the stdx project, smart::hnd
and smart::mappedhnd
declare the strong and weak variants for those wrappers when the destruction policy is a simple call to a Win32 API.
This is certainly true for GDI object handles (like HBRUSH
, HPEN
etc.), where "handles.hpp" defines the HSxxx
and HWxxx
strong and weak variants with simple typedef
-s.
Handling DCs
Device contexts are a different story:
- They can be obtained in a variety of ways, and must be destroyed coherently.
- There is certain "internal dependency" between DCs and GDI objects: when a GDI object is selected into a DC, a DC cannot be destroyed, and a DC cannot be destroyed while having GDI selected objects without causing memory leaks in the GDI.
To overcome these limitations, there is the need of several classes managing the correspondent acquisition and destruction policies. And there is also the need to keep track of the selections, in order to "deselect" before destroy.
Class stdx::smart
(see the referring article), by itself, translates copies and assignments into calls to assign
. A number of policies are defined in the winx::DCs
namespace. The base for all is DCs::Save
.
It declares HDC to be the alias_type
, the value_type
and the acquire_type
, and act as a box for an inner strong pointer to a polymorphic data
class, whose base is Save::data
, that stores strong handles for the selected GDI object and the handle values for the "default DC selected object" (what was in the DC before any selection).
Assigning an HDC makes Save
to allocate a new Save::data
, while copy between Save
makes the data to be shared.
The data deletion policy (Save::data::on_lastrelease
) restores the selected data wit the default values (causing object deselection) and data deletion causes the GDI referred object to be released and –if not else referred- deleted.
This basic behaviour allows to save a DC state and restore it before leaving the DC unreferred. It doesn't delete the DC.
Policy derived from Save
will:
- override the
acquire_type
definition accordingly to their behaviour. - override the
data
structure definition by inheriting theSave
's one and overridingon_last_release
to do proper DC deletion. - override the
assign
functions to create to allocate their owndata
instead of the base one.
In general, different DC policies cannot assign each-other in a heterogeneous way, apart Save
(since all policies derive from it). And since data are passed by smart pointers, the last releasing smart handle will use the policy that data
carries on, that is the one belonging to the data
type created during the first assignment.
Practically, no matter what smart handle causes a DC deletion: the deletion action is always coherent with the creation.
File "handles.hpp" typedef
s the following DC smart handles:
DC_Save
Just a
typedef
forstdx::smart<DCs::Save>
: implements state saving and restoring, and objects retaining at selection.DC_Client
Accepts
HWND
in assignment, callingGetDC
, and callsReleaseDC
on last release.DC_Window
Same as
DC_client
, but usingGetWindowDC
.DC_Paint
Accepts
HWND
, callsBeginPaint
on acquisition andEndPaint
on releasing.DC_Mem
Accepts an
HSBITMAP
, creates a screen-compatible memory DC and selects the bitmap in it.DC_Mem_xy
Accepts a
SIZE
, creates a screen-compatible memory DC, creates a compatible bitmap with those sizes, and selects it in the DC.DC_MemPaint
Accepts an
HWND
, and combinesDC_Paint
andDC_Mem
creating an internalDC_Paint
, a window-like sized bitmap and aDC_Mem
that exposes. The alias DC (where all selection and drawing happens) is the memory DC. On last release, the bitmap is blit-ed in the Paint DC. Practically a double-buffer painter.
Other helpers
To help in certain annoying parameter passing and type definitions, some frequently used Win32 struct
s can be inherited by C++ classes to embed certain shortcuts or some arithmetic.
Class stdx::operators<Derived>
defines all C++ arithmetic operators in terms of assignments and all relations in term of ==
and <
.
Win32 POINT
, SIZE
and RECT
arithmetic can be defined using winx::point
, winx::size
and winx::rect
respectively. All of them inherit stdx::operators
and the respective Win32 struct
, and define assignment operators. They don't redefine Win32 functions: since they auto convert in their respective struct
s, they can be passed as parameters to those functions.
winx::String
The stdx::string
template allows the management of shared sequence of data. When those data are char
or wchar_t
they're nothing more than ordinary strings.
When data are TCHAR
, winx::strings::traits
provides len
and copy
in terms of lstrlen
and lstrcpy
. At that point, winx::String
is the typedef
for stdx::string<TCHAR, winx::strings::traits>
.
File "winstrings.hpp" contains the definitions –in winx
namespace- for some function returning winx::String
, instead of filling a preallocated LPTSTR
.
Message filtering / dispatching
Class winx::msg_filter
must be initialized with a winmsg
(the parameter of on_message
) and contains a set of is_something
functions, that return true if the passed conditions are met.
Hallo world
It can be the following program:
///////////////////////// // w0.cpp #include "stdafx.h" #include "winx/winstrings.hpp" #include "winx/coords.hpp" #include "winx/handles.hpp" #include "winx/win.hpp" #include "winx/loop.hpp" using namespace GE_; class Win: public winx::win { bool on_message(winx::winmsg& M) { winx::msg_filter m(M); return m.is_msg(WM_PAINT) && on_paint(M) || winx::win::on_message(M); } private: bool on_paint(winx::winmsg& M) { winx::DC_MemPaint dc(*this); dc.selectobject(CreateFont(-32,0,0,0,0,0,0,0,0,0,0,0,FF_SWISS,"")); winx::rect rc ; GetClientRect(*this, rc); FillRect(dc, rc, GetSysColorBrush(COLOR_WINDOW)); SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT)); SetBkColor(dc, GetSysColor(COLOR_WINDOW)); DrawText(dc, winx::get_windowtext(*this), -1, rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE); return true; //don't default } }; /////////////////////////////// // WANING: since this is a windows program, to let the WINDOWS subsystem // to enter "main()", mainCRTStartup is declared as // module entry point for the linker. // int main() { STRACE(trc, 0, ("w0 main\n")); stdx::New<Win>()->set_createhook(); return app::msgloop(CreateWindow(app::wndcls::frame(), "Hallo Window!", WS_VISIBLE|WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 600, 400, 0,0, GetModuleHandle(0), 0)).run(); }
We already talked about main
. Now is the time to look at its second code line.
The class Win
is created on heap and its set_createhook
member is called. As a consequence, a Windows hook is installed to spy all the windowproc calls. When CreateWindow
is called, a new HWND
is created and the WM_NCCREATE
message is sent. This condition is seized by the hook that calls the Win::set_hwnd
override, then dismisses itself. (Note: to let this operation to be thread-safe, only one thread is allowed to run during these steps by a Win32 critical-section internally managed.)
The win
implementation of set_hwnd
creates and maps a win::source
, instantiates a winmsglink
and use it to link the source to the Win
instance. It will begin to receive the HWND
messages.
The on_message
implementation returns a complex Boolean expression whose side effect is … dispatching the message: to on_paint
if it is WM_PAINT
or to the base if it is anything else.
The on_paint
function creates a DC_MemPaint
for the HWND
(*this
converts into HWND
automatically), then selects a newly created font.
Note that the HFONT
returned by CreateFont
is converted in HSFONT
by the parameter passing, and stored in the DC policy.
The win::get_windowtext
shortcut calls GetWindowText
using a winx::String
as a buffer. Hence no buffer allocation and handling is required.
At the return
statement, the following effects occur:
dc
is destroyed, and its data released.- The release policy restores the memory DC (thus deselecting the internal bitmap, and the passed font) and deletes it.
- Then, it blits the bitmap that was drawn by the DC into an internally created
PaintDC
(that already calledBeginPaint
on its creation). - Then the data are deleted, causing the bitmap to be deleted and the
PaintDC
to be released. - The release of the
PaintDC
causesEndPaint
to be called.
Conclusions
The usage of the reference counting pattern can greatly simplify Windows programming, allowing an implicit management of resources reducing the risk of leaks in the memory and in the system.
A convenient separation between data and related management (inside classes) and algorithm (namespaced functions) also allows a flexible and extendible way to simplify coding.