How to properly paint in WM_PAINT with GDI+ - c++

I'm new to using GDI+ and I was wondering how to fix the mess of creating and disposing objects that I have. Right now all the program needs to do is handle repeated WM_PAINT messages and update a DrawPie with increased degrees each time.
I have called GdiplusStartup( &gdiplusToken, &gdiplusStartupInput, NULL ); when the window starts up, and GdiplusShutdown( gdiplusToken ); on WM_DESTROY.
Degrees global variable defined near the top:
volatile double degrees = 0;
Here is my WM_PAINT:
case WM_PAINT: {
hdc = BeginPaint( hWnd, &ps );
Graphics g( hdc );
Pen p( Color::Green );
if ( degrees > 0 ) {
if ( degrees == 360 ) {
g.DrawEllipse( &p, 0, 0, 100, 100 );
} else {
g.DrawPie( &p, 0, 0, 100, 100, -90, degrees );
}
}
EndPaint( hWnd, &ps );
break;
}
And here is the function that updates the degrees and updates the window (It's in a separate thread):
void UpdateDegrees() {
for ( ;; ) {
if ( globalHWND != NULL ) {
degrees += 0.1;
InvalidateRect( globalHWND, NULL, TRUE );
UpdateWindow( globalHWND );
}
}
}
If I run it I get a "solid" pie shape like this, which I assume is it just redrawing itself at every angle. It needs to look like this, in other words, clear the graphic before it redraws itself each time. (Sorry, I guess I need 10 rep to post inline images)
I know that I didn't initialize and/or dispose of my graphics correctly, but I honestly have no idea how to do that. Any help would be greatly appreciated! Thanks.

There are lots of ways you can do this, the standard one being to handle the WM_ERASEBKGND message.
For simplicity, you can just fill a white rectangle across the clip rect (which is specified in ps), which will clear the background being painted.
SolidBrush backGroundBrush(Color(255,255,255));
Rect clipRect(ps->rcPaint.left, ps.rcPaint.top, ps->rcPaint.right - ps->rcPaint.left, ps->rcPaint.bottom - ps.rcPaint.top);
g.FillRectangle(&backgroundBrush, Rect(ps->rcPaint.left, ps.rcPaint));

Related

GPU usage jumps to 100% when closing program after drawing a lot of frames with OpenGL

So I've stumbled upon something rather peculiar (after going through a project and fixing memory leaks, which were causing 100% CPU spikes once you closed the program), which seems to cause an incredibly short 100% GPU usage spike, also, once you close the program.
Here's the minimal code I ran to get this:
#undef UNICODE
#include <Windows.h>
#define GLEW_STATIC
#include "include/GL/glew.h"
#include <string>
#include <chrono>
#include <thread>
//
// window
//
HWND window_hwnd = { };
WNDCLASS window_wndclass = { };
std::string wndclass_name = "class";
std::string window_name = "class";
LRESULT CALLBACK WindowProc(
_In_ HWND hWnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
switch (
uMsg
)
{
default:
return DefWindowProc(
hWnd,
uMsg,
wParam,
lParam
);
}
return 0;
}
void window_create_class()
{
window_wndclass = { };
window_wndclass.lpfnWndProc = WindowProc;
window_wndclass.lpszClassName = wndclass_name.c_str();
window_wndclass.cbClsExtra = 0;
window_wndclass.cbWndExtra = 0;
window_wndclass.hInstance = 0;
RegisterClass(
&window_wndclass
);
}
void window_create()
{
window_create_class();
window_hwnd = CreateWindow(
wndclass_name.c_str(),
window_name.c_str(),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
0,
0,
0,
0
);
}
int window_loop()
{
MSG msg;
BOOL get_message;
while (
(
get_message = GetMessage(
&msg,
window_hwnd,
0,
0
) > 0
) != 0
)
{
if (
get_message == -1
)
{
// error handling
break;
}
else
{
TranslateMessage(
&msg
);
DispatchMessage(
&msg
);
}
}
return get_message;
}
//
// opengl
//
HDC gl_hdc = { };
HGLRC gl_hglrc = { };
PIXELFORMATDESCRIPTOR gl_pixel_format_descriptor = {
sizeof(
PIXELFORMATDESCRIPTOR
),
1,
// Flags
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
// The kind of framebuffer. RGBA or palette.
PFD_TYPE_RGBA,
// Colordepth of the framebuffer.
32,
0, 0, 0, 0, 0, 0,
0,
0,
0,
0, 0, 0, 0,
// Number of bits for the depthbuffer
24,
// Number of bits for the stencilbuffer
8,
// Number of Aux buffers in the framebuffer.
0,
PFD_MAIN_PLANE,
0,
0, 0, 0
};
int gl_pixel_format = -1;
void gl_pixel_format_configure()
{
gl_pixel_format = ChoosePixelFormat(
gl_hdc,
&gl_pixel_format_descriptor
);
SetPixelFormat(
gl_hdc,
gl_pixel_format,
&gl_pixel_format_descriptor
);
}
void gl_create()
{
gl_pixel_format_configure();
gl_hglrc = wglCreateContext(
gl_hdc
);
wglMakeCurrent(
gl_hdc,
gl_hglrc
);
GLenum state = glewInit();
if (
state != GLEW_OK
)
{
throw;
}
}
void gl_draw()
{
RECT client_rect = { };
GetClientRect(
window_hwnd,
&client_rect
);
int window_width = client_rect.right - client_rect.left;
int window_height = client_rect.bottom - client_rect.top;
glViewport(
0,
0,
window_width,
window_height
);
glClearColor(
0.0f,
0.0f,
0.0f,
1.0f
);
glClear(
GL_COLOR_BUFFER_BIT
);
wglSwapLayerBuffers(
gl_hdc,
WGL_SWAP_MAIN_PLANE
);
}
int main()
{
window_create();
gl_hdc = GetDC(
window_hwnd
);
gl_create();
ShowWindow(
window_hwnd,
SW_SHOW
);
// simulate drawing frames
auto now = std::chrono::steady_clock::now();
auto interval = std::chrono::duration<
double,
std::chrono::seconds::period
>(
1.0 / 60.0
);
auto next = now + interval;
for (
int i = 0;
i < 400;
i++
)
{
gl_draw();
std::this_thread::sleep_until(
next
);
next += interval;
}
return window_loop();
}
on Windows, using the GLEW library. Compiled with Visual Studio's compiler, with Release optimization. I tested it on my laptop too, which has a completely different GPU (intel integrated graphics, actually) with the same effect.
This does not happen when wglSwapLayerBuffers is not called.
Notice how it doesn't even draw anything, doesn't even create any OpenGL objects, nor are pointers used anywhere. Which makes me wonder, am I using Windows' OpenGL context wrong? Or is this some peculiarity of GLEW? (By the way, I assume the effect of 400 drawn frames (which isn't the exact amount to cause this, I didn't bother trying to pin it down exactly, but it doesn't happen with 300, for example) might differ from GPU to GPU. Though setting it to something like 1000+ should definitely work; it's not even an unrealistic amount for that matter).
To be honest, this isn't exactly an issue (after all, it happens after main returns), but it's still weird, and I'd like to avoid it. So why does it happen, or at the very least how can I fix it?
EDIT #1:
I dabbled around with the OpenGL context creation a little more, creating a proper context as described on the official OpenGL Wiki, instead of the simple one as in the example I provided, with no success.
Curiosity also got the better of me, which made me go through my Steam library and just start up about a dozen games, waiting a few seconds in the main menu, letting them draw a few hundred, if not a few thousand frames.
The majority of games had the same 100% GPU spike once you quit the game. Notably, however, some AAA titles (namely DOOM Eternal and GTA V) did not exhibit this behavior.
I guess, for one, this further drives home the point that this isn't really something to be concerned about. Yet this also proves that it is avoidable, though how exactly to achieve this I still don't know.
I would think the engines running those AAA games have their own OpenGL wrappers and maybe even their own lower level OpenGL-OS interfaces, avoiding WGL, where the issue seems to be stemming from, altogether.
But that's just a guess, I definitely haven't found a concrete answer.
From what I can tell, the GPU will spike to 100% for a split second immediately after closing a program that uses it, including browsers and compilers. I guess this is just the effect of cleaning up the GPU once it's done with a program.
As for why it's happening with your program as compared to other win32 programs I'm pretty sure it's just because you're using GLEW (I might be wrong though).

How to call OnEraseBkgnd() to repaint the window in another function? (MFC)

I am trying to call OnEraseBkgnd() to repaint the window in another function.
For example, like the code below:
...
CBitmap Background;
BITMAP bm;
CDC dcMemory;
int width, height;
...
BOOL CSplashDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CDC* dc;
Background.LoadBitmap(IDB_COVER); //Load Bitmap
Background.GetBitmap(&bm); //Load Bitmap into handle
width = 0;
height = 0;
while(width < 300)
{
width += 10;
height += 10;
OnEraseBkgnd(dc); //<--- Here I want to call OnEraseBkgnd()
Sleep(5); //Delay 5 millisecond
}
return TRUE;
}
BOOL CSplashDlg::OnEraseBkgnd(CDC* pDC)
{
///////////////////////////////////
Invalidate(); //I don't know where I should put this function
///////////////////////////////////
dcMemory.CreateCompatibleDC(pDC);
CBitmap *pOldbmp = dcMemory.SelectObject(&Background);
pDC->SetStretchBltMode(HALFTONE);
pDC->StretchBlt(0, 0, width, height, &dcMemory, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);
return TRUE;
}
In the above example, I want to call OnEraseBkgnd() to repaint the window while executing inside OnInitDialog(). I have searched the Internet and it always says using Invalidate(); or RedrawWindow(); to repaint it and it'll also call the OnEraseBkgnd() function. The question is: How should I use the Invalidate(); or the RedrawWindow();? Where should I put those function?
I have put those two functions in anywhere but it still not works.
EDIT:
I have modified it. Though now Invalidate() and UpdateWindow() are success to call the OnEraseBkgnd() function. But I found another problem: Why didn't the StretchBlt work when I used Invalidate() or UpdateWindow() to repaint it but did the FillSolidRect work?
...
BOOL CMainDlg::OnInitDialog()
{
CSplashDlg Frame;
Frame.width = 0;
Frame.height = 0;
while(Frame.width <= 300)
{
Frame.width += 10;
Frame.height += 10;
Frame.Invalidate(); //<---Here I use both Invalidate() and UpdateWindow()
Frame.UpdateWindow(); //<---Here I use both Invalidate() and UpdateWindow()
Sleep(5); //Delay 5 millisecond
}
CDialog::OnInitDialog();
return TRUE;
}
BOOL CSplashDlg::OnInitDialog()
{
CDialog::OnInitDialog();
width = 0;
height = 0;
Background.LoadBitmap(IDB_COVER); //Load Bitmap
Background.GetBitmap(&bm); //Load Bitmap into handle
return TRUE;
}
BOOL CSplashDlg::OnEraseBkgnd(CDC* pDC)
{
dcMemory.CreateCompatibleDC(pDC);
CBitmap *pOldbmp = dcMemory.SelectObject(&Background);
///////////////////////////////////////////////////////////////
pDC->SetStretchBltMode(HALFTONE);
pDC->StretchBlt(0, 0, width, height, &dcMemory, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY); //It doesn't work when using this one (StretchBlt)
///////////////////////////////////////////////////////////////
pDC->FillSolidRect(0, 0, width, height, RGB(255,0,0)); //But it works when using this one (FillSolidRect)
///////////////////////////////////////////////////////////////
return TRUE;
}
...
You should never call the likes of OnEraseBkgnd( ) and OnPaint( ). MFC will call these when drawing is needed. Your job is to handle the need to draw in OnEraseBkgnd( ).
Invalidate( ) causes MFC to update the drawing area and then it will call OnEraseBkgnd( ) and OnPaint( ). If you Invalidate( ) in OnPaint( ) or OnEraseBkgnd( ) the program will likely hang as it would cause an endless loop of redrawing the window.
You can call Invalidate( ) in OnInitDialog( ) but it is likely unnecessary.
So take the Invalidate out of OnEraseBkgnd( ), don't call OnEraseBkgnd( ) in OnInitDialog( ) and go from there.
You also have to have
ON_WM_PAINT( )
ON_WM_ERASEBKGND( )
in your message map for those to be called by MFC.
NOTE:
I'm not a believer in reinventing wheels. It has mostly been done before. I have not used a splash screen in my projects, but if I were to, I would start here: Splash Screen C++ Class using MFC.
I didn't download and review the code but with four stars, it should be a good place to start. MFC is not an overnight learn. When I started I read books and tons of searches to learn. You just can not make guesses about how the infrastructure works. Hope this helps...

Search icon in edit control overlapped by input area

I am trying to make a search edit control in MFC that has an icon displayed in the control window all the time (regardless the state and text of the control). I have written something like this many years ago and worked very well, but the code no longer works on Windows 7 and newer (maybe even Vista, but did not try that). What happens is that the image shown in the control is overlapped with the input area (see the picture below).
The idea behind the code:
have a class derived from CEdit (that handles painting in OnPaint)
the icon is displayed on the right and the edit area is shrunk based on the size of the icon
resizing is done differently for single-line and multiline edits. For single line I call SetMargins and for multiline edits I call SetRect.
this edit resizing is applied in PreSubclassWindow(), OnSize() and OnSetFont()
This is how the edit input size is applied:
void CSymbolEdit::RecalcLayout()
{
int width = GetSystemMetrics( SM_CXSMICON );
if(m_hSymbolIcon)
{
if (GetStyle() & ES_MULTILINE)
{
CRect editRect;
GetRect(&editRect);
editRect.right -= (width + 6);
SetRect(&editRect);
}
else
{
DWORD dwMargins = GetMargins();
SetMargins(LOWORD(dwMargins), width + 6);
}
}
}
The following image shows the problem with the single line edits (the images have been zoomed in for a better view). The yellow background is for highlighting purposes only, in real code I am using the COLOR_WINDOW system color. You can see that when the single line edit has text and has the input the left side image is painted over. This does not happen with the multiline edit where SetRect correctly sets the formatting rectangle.
I have tried using ExcludeClipRect to remove the area of the edit where the image is being displayed.
CRect rc;
GetClientRect(rc);
CPaintDC dc(this);
ExcludeClipRect(dc.m_hDC, rc.right - width - 6, rc.top, rc.right, rc.bottom);
DWORD dwMargins = GetMargins();
SetMargins(LOWORD(dwMargins), width + 6);
This does not seem to have any effect on the result.
For reference, this is the painting method, written years ago and used to work well on Windows XP, but not correct any more.
void CSymbolEdit::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect( &rect );
// Clearing the background
dc.FillSolidRect( rect, GetSysColor(COLOR_WINDOW) );
DWORD dwMargins = GetMargins();
if( m_hSymbolIcon )
{
// Drawing the icon
int width = GetSystemMetrics( SM_CXSMICON );
int height = GetSystemMetrics( SM_CYSMICON );
::DrawIconEx(
dc.m_hDC,
rect.right - width - 1,
1,
m_hSymbolIcon,
width,
height,
0,
NULL,
DI_NORMAL);
rect.left += LOWORD(dwMargins) + 1;
rect.right -= (width + 7);
}
else
{
rect.left += (LOWORD(dwMargins) + 1);
rect.right -= (HIWORD(dwMargins) + 1);
}
CString text;
GetWindowText(text);
CFont* oldFont = NULL;
rect.top += 1;
if(text.GetLength() == 0)
{
if(this != GetFocus() && m_strPromptText.GetLength() > 0)
{
oldFont = dc.SelectObject(&m_fontPrompt);
COLORREF color = dc.GetTextColor();
dc.SetTextColor(m_colorPromptText);
dc.DrawText(m_strPromptText, rect, DT_LEFT|DT_SINGLELINE|DT_EDITCONTROL);
dc.SetTextColor(color);
dc.SelectObject(oldFont);
}
}
else
{
if(GetStyle() & ES_MULTILINE)
CEdit::OnPaint();
else
{
oldFont = dc.SelectObject(GetFont());
dc.DrawText(text, rect, DT_SINGLELINE | DT_INTERNAL | DT_EDITCONTROL);
dc.SelectObject(oldFont);
}
}
}
I have looked at other implementations of similar edit controls and they all have the same fault now.
Obviously, the question is how do I exclude the image area from the input area of the control?
I think what's going on is that CPaintDC calls BeginPaint(), which sends a WM_ERASEBKGND to the edit box. I wasn't able to ignore it, so I guess it's being sent to maybe an internal STATIC window? Not sure.
Calling ExcludeClipRect() in your OnPaint() handler won't do anything because EDIT will reset the clipping region to the whole client area in either BeginPaint() or its own WM_PAINT handler.
However, EDIT sends a WM_CTRCOLOREDIT to its parent just before painting itself, but seemingly after setting the clipping region. So you can call ExcludeClipRect() there. Sounds like an implementation detail which may change with future versions of the common controls. Indeed, it seems to have done so already.
I did a quick test without MFC on Windows 7, here's my window procedure:
LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM wp, LPARAM lp)
{
switch (m)
{
case WM_CTLCOLOREDIT:
{
const auto dc = (HDC)wp;
const auto hwnd = (HWND)lp;
RECT r;
GetClientRect(hwnd, &r);
// excluding the margin, but not the border; this assumes
// a one pixel wide border
r.left = r.right - some_margin;
--r.right;
++r.top;
--r.bottom;
ExcludeClipRect(dc, r.left, r.top, r.right, r.bottom);
return (LRESULT)GetStockObject(DC_BRUSH);
}
}
return ::DefWindowProc(h, m, wp, lp);
}
I then subclassed the EDIT window to draw my own icon in WM_PAINT, and then forwarded the message so I didn't have to draw everything else myself.
LRESULT CALLBACK edit_wnd_proc(
HWND h, UINT m, WPARAM wp, LPARAM lp,
UINT_PTR id, DWORD_PTR data)
{
switch (m)
{
case WM_PAINT:
{
const auto dc = GetDC(h);
// draw an icon
ReleaseDC(h, dc);
break;
}
}
return DefSubclassProc(h, m, wp, lp);
}
Note that I couldn't call BeginPaint() and EndPaint() (the equivalent of constructing a CPaintDC) in WM_PAINT because the border wouldn't get drawn. I'm guessing it has something to do with calling BeginPaint() twice (once manually, once by EDIT) and the handling of WM_ERASEBKGND. YMMV, especially with MFC.
Finally, I set the margins right after creating the EDIT:
SendMessage(
e, EM_SETMARGINS,
EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(0, margin));
You might also have to update the margins again if the system font changes.
Have a look at this tutorial... from www.catch22.net. It gives a clear picture of how to insert a button into edit control. Though it is an Win32 example, this can be improvised to MFC as the basic structure of MFC is using win32 apis.
http://www.catch22.net/tuts/win32/2001-05-20-insert-buttons-into-an-edit-control/#
It uses WM_NCCALCSIZE to restrict the text control.

Calculate ideal font size, based on the paper size and maximum allowed text length

I have printing code that draws grid on the paper.
Grid has 4 columns, and they have equal horizontal length. Height of the cell is tenth of the paper size. Total number of rows is unknown but I know for a fact that there will be at least one row.
Each cell has same physical size-> width is quarter of the paper width, and height is one tenth of the paper height. Maximum number of characters that can fit into cell is 50.
The problem I face is choosing proper font size so text of maximum length can fit into cell.
Browsing through MSDN documentation and WinAPI examples, I saw that they use GetTextExtPoint32 for similar purposes, but this works only if font already exists and is selected into device context, which is not the case here.
The only thing that crossed my mind was to create "dummy font", see if the example text can fit into cell, and then adjust it's size if the test fails. I have also found this blog that recommends interesting approach to this problem, but being inexperienced I can't decide if "this is the proper way to go".
Can you recommend a correct solution for my problem?
EDITED on June, 30th 2014:
Below is the sample function that draws grid and paints upper left cell in light gray since that cell will contain sample text. That way we can visually validate the success of our drawing code:
// hWnd is the window that owns the property sheet.
HRESULT GDI_PRINT(HWND hWnd)
{
HRESULT hResult;
PRINTDLGEX pdx = {0};
LPPRINTPAGERANGE pPageRanges = NULL;
// Allocate an array of PRINTPAGERANGE structures.
pPageRanges = (LPPRINTPAGERANGE) GlobalAlloc(GPTR, 10 * sizeof(PRINTPAGERANGE));
if (!pPageRanges)
return E_OUTOFMEMORY;
// Initialize the PRINTDLGEX structure.
pdx.lStructSize = sizeof(PRINTDLGEX);
pdx.hwndOwner = hWnd;
pdx.hDevMode = NULL;
pdx.hDevNames = NULL;
pdx.hDC = NULL;
pdx.Flags = PD_RETURNDC;
pdx.Flags2 = 0;
pdx.ExclusionFlags = 0;
pdx.nPageRanges = 0;
pdx.nMaxPageRanges = 10;
pdx.lpPageRanges = pPageRanges;
pdx.nMinPage = 1;
pdx.nMaxPage = 1000;
pdx.nCopies = 1;
pdx.hInstance = 0;
pdx.lpPrintTemplateName = NULL;
pdx.lpCallback = NULL;
pdx.nPropertyPages = 0;
pdx.lphPropertyPages = NULL;
pdx.nStartPage = START_PAGE_GENERAL;
pdx.dwResultAction = 0;
// Invoke the Print property sheet.
hResult = PrintDlgEx(&pdx);
if ( ( hResult == S_OK ) && ( pdx.dwResultAction == PD_RESULT_PRINT ) )
{
// User clicked the Print button,
// so use the DC and other information returned in the
// PRINTDLGEX structure to print the document.
//======= Various initializations ==========//
DOCINFO diDocInfo = {0};
diDocInfo.cbSize = sizeof( DOCINFO );
diDocInfo.lpszDocName = L"Testing printing...";
int pageWidth = GetDeviceCaps( pdx.hDC, HORZRES ),
pageHeight = GetDeviceCaps( pdx.hDC, VERTRES );
//===================== IMPORTANT !!! ==========================//
// Must test this on real printer !!! //
// For now testing is done in XPS and MS OneNote2007 //
//==============================================================//
//================== end of initialization =====================//
if( StartDoc( pdx.hDC, &diDocInfo ) > 0 )
{
if( StartPage( pdx.hDC ) > 0 )
{
//===== creating red pen that will draw grid =====//
LOGBRUSH lb;
lb.lbColor = RGB( 255, 0, 0 );
lb.lbHatch = 0;
lb.lbStyle = BS_SOLID;
HPEN hPen = ExtCreatePen( PS_COSMETIC | PS_SOLID, 1, &lb, 0, NULL);
HGDIOBJ oldPen = SelectObject( pdx.hDC, hPen );
// create test font
HFONT font, oldFont;
long lfHeight = -MulDiv( 14,
GetDeviceCaps( pdx.hDC, LOGPIXELSY ),
72 );
font = CreateFont( lfHeight, 0, 0, 0,
FW_BOLD, TRUE, FALSE, FALSE,
0, 0, 0,
0, 0, L"Microsoft Sans Serif" );
oldFont = SelectFont( pdx.hDC, font );
SetBkMode( pdx.hDC, TRANSPARENT );
SetTextColor( pdx.hDC, RGB( 255, 0, 0 ) );
// testing rectangle -> top left cell of the grid
RECT rcText;
rcText.left = 0;
rcText.top = 0;
rcText.right = pageWidth / 4;
rcText.bottom = pageHeight / 10;
// fill destination rectangle with gray brush
// so we can visually validate rectangle coordinates
FillRect( pdx.hDC, &rcText, (HBRUSH)GetStockObject(LTGRAY_BRUSH) );
// implement solution mentioned in the comment to this question
SIZE s;
::GetTextExtentPoint32( pdx.hDC,
L"Хидрогеотермална енергија Хидрогеотермална енерги",
wcslen( L"Хидрогеотермална енергија Хидрогеотермална енерги" ),
&s );
// select old font back and dispose test font
SelectObject( pdx.hDC, oldFont );
DeleteObject( font );
// adjust font height
lfHeight *= s.cy / ( rcText.bottom - rcText.top );
// now we can create proper font
font = CreateFont( lfHeight, 0, 0, 0,
FW_BOLD, TRUE, FALSE, FALSE,
0, 0, 0,
0, 0, L"Microsoft Sans Serif" );
oldFont = SelectFont( pdx.hDC, font );
// draw text in test rectangle
DrawTextEx( pdx.hDC,
L"Хидрогеотермална енергија Хидрогеотермална енерги",
wcslen( L"Хидрогеотермална енергија Хидрогеотермална енерги" ),
&rcText, DT_CENTER | DT_WORDBREAK | DT_NOCLIP, NULL );
//============== draw a testing grid ===============//
// draw vertical lines of the grid
for( int i = 0; i <= pageWidth; i += pageWidth / 4 )
{
MoveToEx( pdx.hDC, i, 0, NULL );
LineTo( pdx.hDC, i, pageHeight );
}
// draw horizontal lines of the grid
for( int j = 0; j <= pageHeight; j += pageHeight / 10 )
{
MoveToEx( pdx.hDC, 0, j, NULL );
LineTo( pdx.hDC, pageWidth, j );
}
// no need for pen anymore so delete it
SelectObject( pdx.hDC, oldPen );
DeleteObject( hPen );
// no need for font, delete it
SelectFont( pdx.hDC, oldFont );
DeleteFont( font );
if( EndPage( pdx.hDC ) < 0 )
// for now pop a message box saying something went wrong
MessageBox( hWnd, L"EndDoc failed!", L"Error", MB_OK );
}
EndDoc( pdx.hDC );
}
}
if (pdx.hDevMode != NULL)
GlobalFree(pdx.hDevMode);
if (pdx.hDevNames != NULL)
GlobalFree(pdx.hDevNames);
if (pdx.lpPageRanges != NULL)
GlobalFree(pPageRanges);
if (pdx.hDC != NULL)
DeleteDC(pdx.hDC);
return hResult;
}
To use this function, just launch it on button press/menu selection or whatever.
The results in XPS seem consistent, but I get strange results in MS OneNote 2007 which following images illustrate:
Font size is 14 :
Font size is 20 :
Font size is 20, but scaling from the above function was applied :
END OF EDIT
EDITED on July, 6th 2014:
The third picture from above edit was the result of GDI using default height value because the result of my mathematical adjustment for font height was 0. Once zero is passed to CreateFont mentioned behavior is expected.
After performing proper casting from double to int I got nearly perfect output -> last letter in the string barely exceeds the limit. I will continue to try improving this formula since I believe is promising. If anybody has another mathematical solution feel free to post it.
END OF EDIT
If further info / edit is required, leave a comment and I will react as soon as possible.
There are multiple issues involved.
The biggest problem I see is in this line:
lfHeight *= s.cy / ( rcText.bottom - rcText.top );
These are all integers. In C and C++, division with integers results in truncation toward zero. So if the result of the division "should" be 3.7, you'll end up with 3, which can be a pretty crude approximation.
Another problem is that GetTextExtentPoint32 does not wrap text, but DrawText does. So you're measuring the text as though you're going to print it as a single line, and you actually draw it as multiple lines. Instead of using GetTextExtendPoint32, you can measure the height with DrawText by DT_CALCRECT flag.
Putting these together, you want to measure your text like this:
WCHAR szText[] = L"Хидрогеотермална енергија Хидрогеотермална енерги";
RECT rcText;
rcText.left = 0;
rcText.top = 0;
rcText.right = pageWidth / 4;
rcText.bottom = top;
const DWORD options = DT_CENTER | DT_WORDBREAK | DT_NOCLIP;
DrawTextEx( pdx.hDC, szText, -1, &rcText, options | DT_CALCRECT, NULL);
// Because we used DT_CALCRECT, the DrawTextEx call didn't draw anything,
// but it did adjust the bottom of rcText to account for the actual height.
double actual_height = static_cast<double>(rcText.bottom - rcText.top);
double desired_height = pageHeight / 10.0;
double ratio = desired_heigth / actual_height;
// Scale the font height by the ratio, and round it off to the nearest int.
lf.lfHeight = static_cast<int>(lf.lfHeight * ratio + 0.5);
Okay. Basically, I start off with the suggested pointSize (14 in your code) and try to draw the text using the supplied bounding rect. If the text is too large, I go into an iterative loop that decreases the pointsize and measures again until the text will fit into the bounding rect.
If, on the other hand, the text is 'too small' I go into a loop that gradually increases it's size until it is too large. Once I reach this point, I decrease the point-size by 2 and return.
The reduction by 2 is a kludge or hack. I noticed that at times the size was reported as being equal to or smaller than the reported size of the bounding rect, yet still some characters would protrude past the edge of the bounding rect.
A better solution would make use of the DrawTextEx function to both calculate the size and draw the text. This would be better since you could make use of the iLeftmargin and iRightMargin members of the DRAWTEXTPARAMS struct that is passed to that function. Whether you wished to have a margin on each side, or simply wanted to add a single character's width, that you then halved when drawing the text would depend entirely on the desired outcome. I also added the DT_EXTERNALLEADING flag to obtain a small margin above/below the text, though there isn't one for vertical padding, so you'd have to make use of the margin attributes I mention.
Since the DT_VCENTER flag doesn't work with multi-line text, you'd also need to vertically offset the text yourself if you wished it to be vertically centered. You'd just have to offset the rect used for actually drawing the text by half of the difference between the area bounding rect's height and the text bounding rect's height.
I could have used a function like this for a few projects, so thanks for the impetus to actually exercise the grey matter and work it out!
Lastly, I used an interactive demo - one that responded to the WM_PAINT message of a (empty) dialog box. Since a HDC can be treated more-or-less the same whether it be for a printer or the screen, it provided a much quicker way of investigating the result.
Output when plugged into your code: (via cutePDF virtual printer)
Code:
int rectWidth(RECT &r)
{
return (r.right - r.left) + 1;
}
int rectHeight(RECT &r)
{
return (r.bottom - r.top) + 1;
}
void measureFunc(int pointSize, HDC hdc, RECT &pRectBounding, WCHAR *textToDraw, WCHAR *fontFaceName, int &resultWidth, int &resultHeight)
{
int pixelsPerInchY = GetDeviceCaps(hdc, LOGPIXELSY);
int logHeight = -MulDiv(pointSize, pixelsPerInchY, 72);
RECT tmpRect = pRectBounding;
HFONT old, tmp = CreateFont( logHeight, 0, 0, 0, FW_BOLD, TRUE, FALSE, FALSE, 0, 0, 0, 0, 0, fontFaceName );
old = (HFONT)SelectObject(hdc, tmp);
DrawText(hdc, textToDraw, -1, &tmpRect, DT_CENTER | DT_WORDBREAK | DT_NOCLIP | DT_CALCRECT| DT_EXTERNALLEADING );
SelectObject(hdc, old);
DeleteObject(tmp);
resultWidth = rectWidth(tmpRect);
resultHeight = rectHeight(tmpRect);
}
HFONT getMaxFont(HDC hdc, WCHAR *fontName, WCHAR *textToDraw, RECT boundingRect)
{
int maxWidth = rectWidth(boundingRect), maxHeight = rectHeight(boundingRect);
int curWidth, curHeight, pointSize=14;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ( (curWidth>maxWidth) || (curHeight>maxHeight) )
{
bool tooLarge = true;
while (tooLarge)
{
pointSize--;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ((curWidth>maxWidth)||(curHeight>maxHeight))
tooLarge = true;
else
tooLarge = false;
}
}
else
{
bool tooSmall = true;
while (tooSmall)
{
pointSize++;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ( (curWidth<maxWidth) && (curHeight<maxHeight) )
tooSmall = true;
else
tooSmall = false;
}
if ((curWidth>maxWidth) || (curHeight>maxHeight))
{
pointSize-=2;
}
}
int pixelsPerInchY = GetDeviceCaps( hdc, LOGPIXELSY );
int curFontSize;
HFONT result;
curFontSize = -MulDiv(pointSize, pixelsPerInchY, 72);
result = CreateFont(curFontSize, 0, 0, 0, FW_BOLD, TRUE, FALSE, FALSE, 0, 0, 0, 0, 0, fontName );
return result;
}
BOOL CALLBACK DlgMain(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_INITDIALOG:
{
}
return TRUE;
case WM_SIZE:
InvalidateRect(hwndDlg, NULL, true);
return 0;
case WM_ERASEBKGND:
{
RECT mRect;
GetClientRect(hwndDlg, &mRect);
HBRUSH redBrush = CreateSolidBrush(RGB(255,0,0));
FillRect((HDC)wParam, &mRect, redBrush);
DeleteObject(redBrush);
}
return true;
case WM_PAINT:
{
HDC hdc;
PAINTSTRUCT ps;
HFONT requiredFont, oldFont;
WCHAR *textToDraw = L"Хидрогеотермална енергија Хидрогеотермална енерги";
WCHAR *fontFace = L"Microsoft Sans Serif";
RECT boundingRect, dlgRect;
hdc = BeginPaint(hwndDlg, &ps);
oldFont = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
GetClientRect(hwndDlg, &dlgRect);
SetRect(&boundingRect, 0,0, rectWidth(dlgRect) / 4, rectHeight(dlgRect) / 10);
FillRect(hdc, &boundingRect, (HBRUSH)GetStockObject(WHITE_BRUSH));
requiredFont = getMaxFont(hdc, fontFace, textToDraw, boundingRect);
SelectObject(hdc, requiredFont);
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, textToDraw, -1, &boundingRect, DT_CENTER | DT_WORDBREAK | DT_NOCLIP | DT_EXTERNALLEADING );
SelectObject(hdc, oldFont);
DeleteObject(requiredFont);
EndPaint(hwndDlg, &ps);
}
return false;
case WM_CLOSE:
{
EndDialog(hwndDlg, 0);
}
return TRUE;
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
}
}
return TRUE;
}
return FALSE;
}

Static Text Color

I have written Following code which will apply color to all static text in one window, but
I want to apply two different colors in one window like ID:1234 where ID is another color and 1234 will be different color in one window. How can I do this? here is what i have done:
case WM_CTLCOLORSTATIC:
SetBkColor( hdc, COLORREF( :: GetSysColor( COLOR_3DFACE) ) );
//sets bckcolor of static text same as window color
if ( ( HWND ) lParam == GetDlgItem( hWnd, IDC_PID) )
{
SetTextColor( ( HDC ) wParam, RGB( 250, 50, 200));
return ( BOOL ) CreateSolidBrush ( GetSysColor( COLOR_3DFACE) );
}
break;
I'm not sure I understand your problem. Your code looks pretty much ok. One point worth noting is that you are responsible for cleaning up resources that you allocate. With the code above you are leaking the HBRUSH object created through a call to CreateSolidBrush. Since you don't need a custom brush you should rather be using GetSysColorBrush.
As a matter of taste I would filter on the control ID rather than its window handle using GetDlgCtrlID. Incorporating the changes your code should look like this:
case WM_CTLCOLORSTATIC:
switch ( GetDlgCtrlID( (HWND)lParam ) )
{
case IDC_PID:
//sets bckcolor of static text same as window color
SetBkColor( (HDC)wParam, COLORREF( GetSysColor( COLOR_3DFACE ) ) );
SetTextColor( (HDC)wParam, RGB( 250, 50, 200) );
return (INT_PTR)GetSysColorBrush( COLOR_3DFACE );
default:
// Message wasn't handled -> pass it on to the default handler
return 0;
}