Font size scaling problems - c++

I'm writing a C++ wxWidgets calculator application, and I want the font of my wxTextCtrl's and my custom buttons to scale when I resize the window.
The problems are:
The text in my buttons isn't always precisely in the center, but sometimes slightly off (especially in the green and red buttons)
When I maximize the window, the wxTextCtrl's font size updates, but not when I minimize it, leaving it to cover half the screen until I resize the window, at which point it updates to the correct size.
I'm using this code:
text_controls.cpp
MainText = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_READONLY | wxNO_BORDER);
MainText->SetForegroundColour(wxColour(55, 55, 55));
MainText->Bind(wxEVT_TEXT, &Main::OnTextChange, this);
MainText->Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) {
evt.Skip();
MainText->SetFont(
wxFontInfo(wxSize(0, MainText->GetSize().y / 1.3))
.Family(wxFONTFAMILY_SWISS)
.FaceName("Lato")
.Bold()
);
});
And I'm using very similar code to generate the font in my custom button class file:
ikeButton.cpp
void ikeButton::render(wxDC& dc)
{
unsigned int w = this->GetSize().GetWidth();
unsigned int h = this->GetSize().GetHeight();
wxColour* bCol;
if (pressed) {
dc.SetBrush(*pressedBackgroundColor);
dc.SetTextForeground(*pressedTextColor);
bCol = pressedBorderColor;
}
else if (hovered) {
dc.SetBrush(*hoveredBackgroundColor);
dc.SetTextForeground(*hoveredTextColor);
bCol = hoveredBorderColor;
}
else {
dc.SetBrush(*backgroundColor);
dc.SetTextForeground(*textColor);
bCol = borderColor;
}
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRectangle(0, 0, w, h);
//bordo
if (borderTh && bCol != NULL)
{
dc.SetBrush(*bCol);
dc.DrawRectangle(0, 0, w, borderTh);
dc.DrawRectangle(w - borderTh, 0, borderTh, h);
dc.DrawRectangle(0, h - borderTh, w, borderTh);
dc.DrawRectangle(0, 0, borderTh, h);
}
//testo
dc.SetFont(
wxFontInfo(wxSize(0, this->GetSize().GetHeight() / 3))
.Family(wxFONTFAMILY_SWISS)
.FaceName("Lato")
.Light()
);
dc.DrawText(text, w / 2 - (GetTextExtent(text).GetWidth()),
h / 2 - (GetTextExtent(text).GetHeight()));
}

I'm really not sure what to do about the second problem. I think setting the font size from the size event when the frame is unmaximized causes something to be done in an unexpected order and temporarily breaks wxWidgets' layout system.
The only way I've found to workaround this so far is to use WinAPI calls to inject an extra Layout call when the window is restored. To do this, add this declaration to your frame class:
#ifdef __WXMSW__
bool MSWHandleMessage(WXLRESULT *result,WXUINT message,
WXWPARAM wParam, WXLPARAM lParam) override;
#endif // __WXMSW__
The body of the MSWHandleMessage method will need some extra constants that are defined in the wrapwin.h header. So in the code file for the frame, have this be the last #include line:
#ifdef __WXMSW__
#include <wx/msw/wrapwin.h>
#endif // __WXMSW__
And then add this body for the MSWHandleMessage
#ifdef __WXMSW__
bool MyFrame::MSWHandleMessage(WXLRESULT *result, WXUINT message,
WXWPARAM wParam, WXLPARAM lParam)
{
if ( message == WM_SYSCOMMAND && wParam == SC_RESTORE )
{
CallAfter([this](){Layout();});
// We still want to do the default processing, so do not set a result
// and return true.
}
return wxFrame::MSWHandleMessage(result, message, wParam, lParam);
}
#endif // __WXMSW__
Obviously change 'MyFrame' to the name of your frame class.
But I can help with the first one.
I think there are 2 small problems with the current calculation of where to draw the text. First, I think GetTextExtent should be called on the dc instead of the window. Second, I think there is a slight problem with order of the math operations. To center the text, I think the calculation for the x coordinate should be (w - GetTextExtent(text).GetWidth()) / 2. A similar change should be made in the calculation for the y coordinate.
I would also store the text extent calculation instead of doing it twice:
wxSize textExtent = dc.GetTextExtent(text);
dc.DrawText(text, (w - textExtent.GetWidth())/ 2,
(h - textExtent.GetHeight())/2);
Feel free to skip this next part.
You can sometimes center text vertically better by basing the calculation on a font metric named ascent instead of the text height. Here's a diagram of what these font metrics mean from Microsoft
The reason ascent might be a better number to use is that the height will include several padding elements that might make the text look slightly uncentered.
wxSize textExtent = dc.GetTextExtent(text);
wxFontMetrics metrics = dc.GetFontMetrics();
dc.DrawText(text, (w - textExtent.GetWidth()) / 2, (h - metrics.ascent) / 2);

Related

How to align window to windows' taskbar

I'm trying to position my Win32 API window alongside (e.g. left-sided vertical) taskbar. My display has 2560x1600 resolution and 144 DPI. I had some problems with DPI-aware apps previously, so maybe I still don't undestand some DPI-related things. For example, now I set DPI-awarness both programmatically - setting the DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 option through the Win32 API, and adding two lines (to support Windows 7-10) to the project's manifest file:
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
Here is a code snippet, that shows how I'm setting a window's position:
RECT taskbarRect = {0, 0, 0, 0};
HWND taskbarHandle = FindWindow(L"Shell_TrayWnd", NULL);
if (taskbarHandle) {
GetWindowRect(taskbarHandle, &taskbarRect);
} else {...}
RECT notificationWindowRect; // Here is the RECT for window, I'm trying to reposition.
GetWindowRect(notificationWindowHandle, &notificationWindowRect);
LONG newX = 0;
LONG newY = 0;
bool taskbarIsVertical = (taskbarRect.Height() > taskbarRect.Width());
if (taskbarRect.left == 0 && taskbarIsVertical) { // left vertical taskbar
newX = taskbarRect.right;
newY = taskbarRect.bottom - notificationWindowRect.Height();
} else {...}
SetWindowPos(notificationWindowHandle, NULL, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
When system scaling is set to 100%, it's almost working - taskbarRect has a width of 63, but still there is a gap of a few pixels between the taskbar's right side and my window's left side. Note, that my window has the popup style and has no borders.
However, the main problem happens, when I set Windows' scaling to 150%. From the one hand, the taskbarRect's width becomes equal to 94, which I suppose is correct because 63 * 1.5 == 94. On the other hand, my window becomes hidden a little bit from the left side by the taskbar. To handle that, I need to add 65 pixels:
newX = taskbarRect.right + 65;
I don't understand where this 65-pixel shift appears from, and why it is exactly 65 pixels.

Any way to retrieve text height, in pixels, of text within a CEditBox?

I have an edit box that contains text, sometimes many sentences long. The edit box sits at the bottom of its parent dialog (forgive me if I'm saying everything wrong, I don't quite know what I'm doing when it comes to MFC applications). When the dialog that contains my edit box in mind is drawn to the screen, it isn't drawn quite tall enough, and it cuts off a portion of my edit box at the bottom. I was hoping to be able to calculate the height of the text that is used in the edit box, and add a few multiples of that value to the function that determines the height of the parent dialog, for consistency.
I'm not sure if this makes sense, but ultimately I am just trying to find out if it's possible to get text height of text within my edit box. I'm not sure that my fix is even possible given that the edit box is created in a completely different file in the project, but I thought it might be worth asking.
You could calculate the required text height using this basic formula:
CEdit::GetLineCount() * TEXTMETRIC::tmHeight
If the edit control has any of WS_BORDER or WS_HSCROLL styles you have to account for the gap between window size and content size which can be calculated by taking the difference between the heights of the rectangles returned by CEdit::GetWindowRect() and CEdit::GetRect() (thanks Barmak!).
The following is a function to calculate the "ideal" size of an edit control. The returned height is the required window height to fit the content. The returned width equals the original window width. You can use the parameters minLines and maxLines to make sure the returned height is such that the edit control shows at least minLines and at maximum maxLines number of lines without scrolling. Leave them at their defaults to not restrict the height.
CSize GetEditIdealSize( CEdit& edit, unsigned minLines = 0, unsigned maxLines = 0 )
{
if( CFont* pFont = edit.GetFont() )
{
// Get font information.
CClientDC dc( &edit );
auto const pOldFont = dc.SelectObject( pFont );
TEXTMETRICW tm{}; dc.GetTextMetricsW( &tm );
if( pOldFont )
dc.SelectObject( pOldFont );
// Calculate required height for the text content.
int const heightRequired = edit.GetLineCount() * tm.tmHeight;
// Make sure the edit control height stays between the given minimum/maximum.
int idealHeight = std::max<int>( heightRequired, tm.tmHeight * minLines );
if( maxLines > 0 )
idealHeight = std::min<int>( idealHeight, tm.tmHeight * maxLines );
// Get window and content rect.
CRect rcEdit; edit.GetWindowRect( rcEdit );
CRect rcContent; edit.GetRect( rcContent );
// Account for gap between window rect and content rect.
idealHeight += rcEdit.Height() - rcContent.Height();
return { rcEdit.Width(), idealHeight };
}
return {};
}
Use it like this in a member function of the parent window of the edit control to resize the edit control to fit its content:
CSize const idealSize = GetEditIdealSize( m_edit );
m_edit.SetWindowPos( nullptr, 0, 0, idealSize.cx, idealSize.cy, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE );
This code has been tested under Windows 10 for an edit control with the style ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_BORDER | WS_VISIBLE | WS_CHILD.

Windows: Getting a window title bar's height

I was trying to get the height of the title bar of a specific window on Windows. You can replicate it with Notepad. I'm using C++ and none of the codes I found online yielded the correct result. Using e.g. Screenpresso I measured 31 pixels for my window bar height.
The functions I tried are the following:
TitleBarHeight.h:
#pragma once
#include <windows.h>
inline int get_title_bar_thickness_1(const HWND window_handle)
{
RECT window_rectangle, client_rectangle;
GetWindowRect(window_handle, &window_rectangle);
GetClientRect(window_handle, &client_rectangle);
return window_rectangle.bottom - window_rectangle.top -
(client_rectangle.bottom - client_rectangle.top);
}
inline int get_title_bar_thickness_2(const HWND window_handle)
{
RECT window_rectangle, client_rectangle;
GetWindowRect(window_handle, &window_rectangle);
GetClientRect(window_handle, &client_rectangle);
return (window_rectangle.right - window_rectangle.left - client_rectangle.right) / 2;
}
Results:
auto window_handle = FindWindow("Notepad", nullptr);
auto a = get_title_bar_thickness_1(window_handle); // 59
auto b = get_title_bar_thickness_2(window_handle); // 8
auto c = GetSystemMetrics(SM_CXSIZEFRAME); // 4
auto d = GetSystemMetrics(SM_CYCAPTION); // 23
Getting the system metrics with GetSystemMetrics() does not work because windows can have different title bar heights obviously and there is no argument for the window handle.
How can I really get the result of 31?
Assuming that you don't have menu bar, you can map points from client coordinate system to screen one
RECT wrect;
GetWindowRect( hwnd, &wrect );
RECT crect;
GetClientRect( hwnd, &crect );
POINT lefttop = { crect.left, crect.top }; // Practicaly both are 0
ClientToScreen( hwnd, &lefttop );
POINT rightbottom = { crect.right, crect.bottom };
ClientToScreen( hwnd, &rightbottom );
int left_border = lefttop.x - wrect.left; // Windows 10: includes transparent part
int right_border = wrect.right - rightbottom.x; // As above
int bottom_border = wrect.bottom - rightbottom.y; // As above
int top_border_with_title_bar = lefttop.y - wrect.top; // There is no transparent part
Got 8, 8, 8 and 31 pixels (96DPI aka 100% scaling setting)
You should also take into account DPI awareness mode. Especially GetSystemMetrics is tricky because it remembers state for System DPI when your application was launched.
Send a message WM_GETTITLEBARINFOEX to the window, and you will get the bounding rectangle of the title bar.
TITLEBARINFOEX * ptinfo = (TITLEBARINFOEX *)malloc(sizeof(TITLEBARINFOEX));
ptinfo->cbSize = sizeof(TITLEBARINFOEX);
SendMessage(hWnd, WM_GETTITLEBARINFOEX,0, (LPARAM)ptinfo);
int height = ptinfo->rcTitleBar.bottom- ptinfo->rcTitleBar.top;
int width = ptinfo->rcTitleBar.right - ptinfo->rcTitleBar.left;
free(ptinfo);
First, make sure your application is high DPI aware so that the system doesn't lie to you.
Options:
Trust GetSystemMetrics. Nearly any top-level window that actually has a different caption size is doing custom non-client area management which is going to make it (nearly) impossible. The obvious exception is a tool window (WS_EX_TOOLWINDOW) which probably has a SM_CYSMCAPTION height if the WS_CAPTION style is also set.
Get the target window rect and the target window's style. Use AdjustWindowRectEx to determine the size differences with the WS_CAPTION style toggled. I'm not sure if this will work because there may be some interaction between on whether you can have a caption without some kind of border.
Get the target window rect and send WM_HITTEST messages for coordinates that move down the window. Count how many of those get HT_CAPTION in return. Bonus points if you do this with a binary search rather than a linear search. This is probably the hardest and the most reliable way to do it, assuming the window has a rectangular caption area.
If I've understood correctly, it looks like you want to take the border size of the window (which we should be able to gather from the width as there is no title bar) and subtract it from the the verticle size minus the client window...
inline int get_title_bar_thickness(const HWND window_handle)
{
RECT window_rectangle, client_rectangle;
int height, width;
GetWindowRect(window_handle, &window_rectangle);
GetClientRect(window_handle, &client_rectangle);
height = (window_rectangle.bottom - window_rectangle.top) -
(client_rectangle.bottom - client_rectangle.top);
width = (window_rectangle.right - window_rectangle.left) -
(client_rectangle.right - client_rectangle.left);
return height - (width/2);
}

CClientDC and DC not Drawing on ChtmlEditCtrl

Hi all I am working with a CHtmlEditCtrl in MFC. I want to draw some random rectangles and lines inside a function handling right click event.
The ChtmlEditCtrl control is created from static using this snippet:
bool CHtmlEditCtrlEx::CreateFromStatic( UINT nID, CWnd* pParent ) {
CStatic wndStatic;
if ( !wndStatic.SubclassDlgItem(nID, pParent)) {
return false;
}
CRect rc;
wndStatic.GetWindowRect( &rc );
pParent->ScreenToClient( &rc );
if (Create( 0, (WS_CHILD | WS_VISIBLE), rc, pParent, nID, 0 )) {
...
}
Then I override the CWnd::pretranslate() function as thus:
CClientDC dcc(this);
switch (pMsg->message) {
case WM_RBUTTONUP: // Right-click
// Just some dummy values
DrawSquigly(dcc, 600, 240, 20);
break;
}
the DrawSquigly() function is defined as thus:
void CHtmlEditCtrlEx::DrawSquigly(CDC &dcc, int iLeftX, int iWidth, int iY)
{
CAMTrace trace;
trace.Trace("Drawing Squiggly");
//dcc.TextOut(10, 10, CString(_T("I used a client DC!")));
CPen * oldPen;
CBrush * oldBrush;
oldPen = (CPen *) dc.SelectStockObject(WHITE_PEN);
dcc.MoveTo(5,10);
dcc.LineTo(80, 10);
dcc.SelectObject(oldPen);
//GDI 002_2: Create custom pen with different Line thickness.
CPen thick_pen(PS_SOLID, 3, RGB(0,255,0));
oldPen = dc.SelectObject(&thick_pen);
dcc.MoveTo(5, 20);
dcc.LineTo(80,20);
dcc.SelectObject(oldPen);
//GDI 002_3: Create a Rectangle now
dcc.Draw3dRect(5,30,80,70, RGB(25,25,255), RGB(120,120,120));
//GDI 002_4: Create a Brush that we can use for filling the
// closed surfaces
CBrush brush(RGB(255,0,255));
oldBrush = dc.SelectObject(&brush);
dcc.Rectangle(5,110,80,140);
dcc.SelectObject(oldBrush);
//GDI 002_5: Hatch Brush is useful to apply a pattern in stead
//of solid fill color
CBrush* hatBrush = new CBrush();
hatBrush->CreateHatchBrush(HS_CROSS, RGB(255,0,255));
oldBrush = dc.SelectObject(hatBrush);
dcc.FillRect(new CRect(5,160,80,190), hatBrush);
dcc.SelectObject(oldBrush);
}
but no drawing happens when I right click. I think I am missing something especially because I am new to MFC.
I have added a trace to the top of the event handler to be sure that the function is getting called and it is.
Can anyone please point me the right direction?
There are actually 2 device context in your code: one you pass as parameter in the call (we don't know where it comes from) and the other created locally in the drawing function.
Normally, when the systems gives you a DC it expects you draw something in it, not that you draw into something else.
If the window you are working on is layered, the system gives you a memory context you draw in that is - upon clearing - blit-ted onto the window itself with some window manager effect.
My suspect is that -by allocating a second dc- your drawing are ovewritten when the first one (you left blank) is cleared upon returning from the message handler.

MFC - dim main window when showing modal dialog

I have a fairly standard MFC application that consists of a main window, and occasionally brings up modal dialogs. As we all know nothing can be done outside a modal dialog until it is closed.
Therefore, a nice UI feature is to "dim" the rest of the main window behind the dialog, to visually indicate you can't use it until you're done with the modal dialog. Some web apps and java/mac apps do this, but I've never seen it done in a traditional C++/MFC application. I'd like to give it a try, even if it's unusual for the platform.
How can this be done? I have several modal dialogs in the application, used in this pattern:
// pMainFrame is available as a pointer to the CWnd of the main window
CMyDialog dialog;
dialog.DoModal(); // invoke modal dialog; returns after dialog closed
Is there an easy way to have the window dimmed before any DoModal() and restored afterwards? I'm using Visual Studio 2010 in case the updated MFC has any features that might help.
Edit: I've posted a solution based on oystein's answer, but I'm starting a bounty in case anyone can improve on it - especially with a smooth fade in/fade out.
You can create another window, completely black, on top of the window you want to dim, and set the black window's opacity with SetLayeredWindowAttributes. It doesn't have to be black, of course, but I guess that's the best dimming color.
EDIT: I hacked together an example - but note that I am not an MFC developer, I usually use the Windows API directly. It seems to work okay, though.
Here is a pastebin. Feel free to add fade-ins etc. yourself. Also note that this dims the entire screen, you'll have to resize my dimming-window if you don't want this behaviour. See code comments.
/**********************************************************************************************
MFC screen dim test
:: oystein :: November 2010
Creates a simple window - click it to toggle whether a translucent black "dimmer" window
is shown. The dimmer-window covers the entire screen, but the taskbar ("superbar" in
Windows 7) will jump on top of it if clicked - it seems. Simple suggestions to fix that
are welcome.
Should work on Windows 2000 and later.
Disclaimer: This is my first MFC program ever, so if anything seems wrong, it probably is.
I have previously only coded with pure Win32 API, and hacked this together using online
tutorials. Code provided "as-is" with no guarantees - I can not be held responsible for
anything bad that happens if you run this program.
***********************************************************************************************/
#include "stdafx.h"
#undef WINVER
#define WINVER 0x500 // Windows 2000 & above, because of layered windows
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
//
// Black window used to dim everything else
//
class CDimWnd : public CFrameWnd
{
public:
CDimWnd()
{
// Get screen res into rect
RECT rc;
GetDesktopWindow()->GetWindowRect(&rc);
CreateEx(WS_EX_LAYERED | // Layered window for translucency
WS_EX_TRANSPARENT | // Click through
WS_EX_TOPMOST | // Always on top
WS_EX_TOOLWINDOW, // Do not appear in taskbar & similar
NULL, TEXT(""),
WS_POPUP, // No frame/borders - though there is
// still some border left - we'll remove
// it with regions
0, 0, rc.right + 10, rc.bottom + 10, // Make the window 10px larger
// than screen resolution in both
// directions - it is still positioned
// at 0,0
NULL, NULL);
// Grab a part of the window the size of the desktop - but 5px into it
// Because the window is larger than the desktop res, the borders are removed
CRgn rgn;
rgn.CreateRectRgn(rc.left + 5, rc.top + 5, rc.right + 5, rc.bottom + 5);
SetWindowRgn((HRGN)rgn, FALSE);
rgn.Detach();
// We have to reposition window - (0,0) of window has not changed
SetWindowPos(NULL, -5, -5, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
// This is where we set the opacity of the window: 0-255
SetLayeredWindowAttributes(RGB(0,0,0), 150, LWA_ALPHA);
}
void Close()
{
CFrameWnd::OnClose();
}
BOOL CDimWnd::OnEraseBkgnd(CDC* pDC); // Set BKG color
DECLARE_MESSAGE_MAP()
};
BOOL CDimWnd::OnEraseBkgnd(CDC* pDC)
{
// Set brush to desired background color
CBrush backBrush(RGB(0, 0, 0));
// Save old brush
CBrush* pOldBrush = pDC->SelectObject(&backBrush);
CRect rect;
pDC->GetClipBox(&rect); // Erase the area needed
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY);
pDC->SelectObject(pOldBrush);
return TRUE;
}
BEGIN_MESSAGE_MAP(CDimWnd, CFrameWnd)
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Global variable - is screen dimmed?
bool g_bIsDimmed = false;
// The main window
class CMainWnd : public CFrameWnd
{
// Contains a CDimWnd - I'm not sure if this is the "MFC way" of doing things
CDimWnd dimmer;
public:
CMainWnd()
{
Create(NULL, TEXT("Screen dimmer - Press left mouse button on window to toggle"),
WS_OVERLAPPEDWINDOW, CRect(50, 50, 400, 250));
}
// Left mouse button toggles dimming
afx_msg void OnLButtonDown(UINT Flags, CPoint Point)
{
if(!g_bIsDimmed)
{
dimmer.ShowWindow(SW_SHOW);
dimmer.BringWindowToTop();
g_bIsDimmed = true;
}
else
{
dimmer.ShowWindow(SW_HIDE);
g_bIsDimmed = false;
}
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMainWnd, CFrameWnd)
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
// The app
class CApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
BOOL CApp::InitInstance()
{
m_pMainWnd = new CMainWnd();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
CApp HelloApp;
UPDATE:
I hacked together some more code for you, to handle the fading. I'm still no MFC dev, and I left the code in a "rough" state (little error handling, not very robust) to give you something to do too. :) Anyway, here's one way to do it, that I think is fairly clean:
To use it, make your main window contain a dimmer window
class CMainFrm : public CFrameWnd
{
CDimWnd* dimmer;
public:
CMainFrm()
{
// constructor code here ...
dimmer = new CDimWnd();
}
// rest of class ...
};
It can then be used e.g. like this:
dimmer->Show();
MessageBox(TEXT("Hello world"));
dimmer->Hide();
Alternatively I guess you could put this code (Show()/Hide() calls) in the constructor and destructor of the modal dialog, if you want to keep the code there. If you want a "scope"-dim, like in the example you posted, this code would have to go in the constructor & destructor of the CDimWnd class, and you would need something like a static member variable to ensure that only one dimmer is running at a time (unless you want to use a global variable).
For the dimmer window - I did this:
CDimWnd.h
#define TARGET_OPACITY 70 // Target opacity 0-255 for dimmed window
#define FADE_TIME 20 // Time between each fade step in milliseconds
#define FADE_STEP 5 // How much to add to/remove from opacity each fade step
#define ID_FADE_TIMER 1
// Call Show() and Hide() to fade in/fade out dimmer.
// Creates the dimmer window in constructor.
class CDimWnd : public CFrameWnd
{
bool m_isDimming;
public:
CDimWnd();
void Show();
void Hide();
protected:
BOOL OnEraseBkgnd(CDC* pDC);
void OnTimer(UINT_PTR nIDEvent);
DECLARE_MESSAGE_MAP()
};
CDimWnd.cpp
#include "stdafx.h"
#include "CDimWnd.h"
#include "MainFrm.h"
BEGIN_MESSAGE_MAP(CDimWnd, CFrameWnd)
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
CDimWnd::CDimWnd()
{
// Get the main frame of the application which we want to dim.
CMainFrame* pParent = theApp.pMainFrame;
// Don't do anything if the main frame doesn't appear to be there
if (pParent != NULL)
{
// Get the client area of the window to dim.
CRect rc;
pParent->GetClientRect(&rc);
pParent->ClientToScreen(&rc); // convert to screen coordinates
// Do some fudging to fit the client area exactly.
// Other applications may not need this if the above client area fits already.
rc.top += GetSystemMetrics(SM_CYFRAME);
rc.top += GetSystemMetrics(SM_CYCAPTION); // MFC feature pack seems to include caption in client area
rc.left -= GetSystemMetrics(SM_CXBORDER);
rc.right += GetSystemMetrics(SM_CXBORDER) + 1;
rc.bottom += GetSystemMetrics(SM_CYBORDER) + 1;
// Create a layered window for transparency, with no caption/border.
CreateEx(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW, NULL, TEXT(""),
WS_POPUP, rc.left, rc.top, rc.Width(), rc.Height(),
pParent->GetSafeHwnd(), NULL);
}
}
void CDimWnd::Show()
{
// If we are not already dimming, go for it
if(!m_isDimming)
{
// Bring in front of main window.
BringWindowToTop();
// Set opacity to 0
SetLayeredWindowAttributes(RGB(0,0,0), 0, LWA_ALPHA);
// Show the dimmer window
ShowWindow(SW_SHOW);
// Create timer - the rest is handled in OnTimer() function
SetTimer(ID_FADE_TIMER, FADE_TIME, NULL);
}
}
void CDimWnd::Hide()
{
// If we are dimming, go for it
if(m_isDimming)
{
// Create timer - the rest is handled in OnTimer() function
SetTimer(ID_FADE_TIMER, FADE_TIME, NULL);
}
}
void CDimWnd::OnTimer(UINT_PTR nIDEvent)
{
static int fade = 0;
if(nIDEvent == ID_FADE_TIMER)
{
// We are dimming => we want to fade out
if(m_isDimming)
{
if(fade < 0)
{
// Fading finished - hide window completely, update status & destroy timer
fade = 0;
ShowWindow(SW_HIDE);
KillTimer(nIDEvent);
m_isDimming = false;
}
else
{
// Set window opacity & update fade counter
SetLayeredWindowAttributes(RGB(0,0,0), fade, LWA_ALPHA);
fade -= FADE_STEP;
}
}
else
// fade in
{
if(fade > TARGET_OPACITY)
{
// Fading finished - destroy timer & update status
fade = TARGET_OPACITY; // but first, let's be accurate.
SetLayeredWindowAttributes(RGB(0,0,0), fade, LWA_ALPHA);
KillTimer(nIDEvent);
m_isDimming = true;
}
else
{
// Set window opacity & update fade counter
SetLayeredWindowAttributes(RGB(0,0,0), fade, LWA_ALPHA);
fade += FADE_STEP;
}
}
}
}
BOOL CDimWnd::OnEraseBkgnd(CDC* pDC)
{
// Fill with black
CBrush backBrush(RGB(0, 0, 0));
CBrush* pOldBrush = pDC->SelectObject(&backBrush);
CRect rect;
pDC->GetClipBox(&rect); // Erase the area needed
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY);
pDC->SelectObject(pOldBrush);
return TRUE;
}
Okay. As I said, this was thrown together fairly quickly and is in a rough state, but it should give you some code to work from, and a general idea of how (I think) timers are used in MFC. I am definitely not the right person to think anything about that, though :)
I've accepted oystein's answer, since it led me to the solution, but I thought I'd post my modifications. I had to modify it a bit to make it work for me, so it might come in useful to someone else.
For the record, the dimming works well, but it doesn't look as natural as I hoped. In an application which frequently brings up dialogs, the dimming becomes distracting in its regularity of seemingly switching the main window on and off. To compromise, I've made the dimming fairly subtle (about 25% opacity) which gently highlights the active dialog; the instant dimming is still a little distracting, but I'm not sure how to have it fade in or fade out smoothly, especially when scoped.
Also, I'm not a UI expert, but the dimming gave me a sort of impression that the dialog was less related to the window content behind it. This made it feel a bit detached from what I was working on in the application, even though the dialogs are directly manipulating that content. This might be another distraction.
Here it is anyway:
CDimWnd.h
// Dim the application main window over a scope. Creates dimmer window in constructor.
class CDimWnd : public CFrameWnd
{
public:
CDimWnd();
BOOL OnEraseBkgnd(CDC* pDC);
~CDimWnd();
protected:
DECLARE_MESSAGE_MAP()
};
CDimWnd.cpp
#include "stdafx.h"
#include "CDimWnd.h"
#include "MainFrm.h"
BEGIN_MESSAGE_MAP(CDimWnd, CFrameWnd)
ON_WM_ERASEBKGND()
END_MESSAGE_MAP()
// For preventing two dimmer windows ever appearing
bool is_dimmer_active = false;
CDimWnd::CDimWnd()
{
// Get the main frame of the application which we want to dim.
CMainFrame* pParent = theApp.pMainFrame;
// Don't do anything if the main frame doesn't appear to be there,
// or if there is already dimming happening.
if (pParent != NULL && !is_dimmer_active)
{
// Get the client area of the window to dim.
CRect rc;
pParent->GetClientRect(&rc);
pParent->ClientToScreen(&rc); // convert to screen coordinates
// Do some fudging to fit the client area exactly.
// Other applications may not need this if the above client area fits already.
rc.top += GetSystemMetrics(SM_CYFRAME);
rc.top += GetSystemMetrics(SM_CYCAPTION); // MFC feature pack seems to include caption in client area
rc.left -= GetSystemMetrics(SM_CXBORDER);
rc.right += GetSystemMetrics(SM_CXBORDER) + 1;
rc.bottom += GetSystemMetrics(SM_CYBORDER) + 1;
// Create a layered window for transparency, with no caption/border.
CreateEx(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW, NULL, TEXT(""),
WS_POPUP, rc.left, rc.top, rc.Width(), rc.Height(),
pParent->GetSafeHwnd(), NULL);
// Bring in front of main window.
BringWindowToTop();
// Apply 25% opacity
SetLayeredWindowAttributes(RGB(0,0,0), 64, LWA_ALPHA);
// Show the dimmer window
ShowWindow(SW_SHOW);
is_dimmer_active = true;
}
}
CDimWnd::~CDimWnd()
{
is_dimmer_active = false;
}
BOOL CDimWnd::OnEraseBkgnd(CDC* pDC)
{
// Fill with black
CBrush backBrush(RGB(0, 0, 0));
CBrush* pOldBrush = pDC->SelectObject(&backBrush);
CRect rect;
pDC->GetClipBox(&rect); // Erase the area needed
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY);
pDC->SelectObject(pOldBrush);
return TRUE;
}
Usage is dead simple: because CDimWnd creates itself in its constructor, all you need to do is add CDimWnd dimmer as a member of the dialog class, and it automatically dims the main window, no matter where you call the dialog from.
You can also use it within a scope to dim system modal dialogs:
{
CDimWnd dimmer;
MessageBox(...);
}
I couldn't resist doing it.
It's your code with added timers and implemented a fade in / fade out. Also I changed to use mid grey rather than black for the obscuring block.
You can tweak the constants that control the fade to make it smoother by increasing the duration or the increasing the rate. Experiment shows me that a rate of 10hz is smooth for me, but YMMV
// DimWnd.h : header file
#pragma once
class CDimWnd : public CFrameWnd
{
public:
CDimWnd(class CWnd * pParent);
virtual ~CDimWnd();
BOOL OnEraseBkgnd(CDC* pDC);
int opacity, opacity_increment;
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnTimer(UINT_PTR nIDEvent);
void fadeOut();
};
// DimWnd.cpp : implementation file
//
#include "stdafx.h"
#include "dimmer.h"
#include "DimWnd.h"
#include "MainFrm.h"
#include <math.h>
const int TIMER_ID = 111;
// For preventing two dimmer windows ever appearing
bool is_dimmer_active = false;
// constants to control the fade.
int ticks_per_second = 1000; // ms
int start_opacity = 44; // 20%
int max_opacity = 220; // 0->255
double fade_in_duration = 4; // seconds to fade in (appear)
double fade_out_duration = 0.2; // seconds to fade out (disappear)
int rate = 100; // Timer rate (ms
CDimWnd::CDimWnd(CWnd * pParent)
{
// Don't do anything if the main frame doesn't appear to be there,
// or if there is already dimming happening.
if (pParent != NULL && !is_dimmer_active)
{
// Get the client area of the window to dim.
CRect rc;
pParent->GetClientRect(&rc);
pParent->ClientToScreen(&rc); // convert to screen coordinates
// Do some fudging to fit the client area exactly.
// Other applications may not need this if the above client area fits already.
rc.top += GetSystemMetrics(SM_CYFRAME);
rc.top += GetSystemMetrics(SM_CYCAPTION); // MFC feature pack seems to include caption in client area
rc.left -= GetSystemMetrics(SM_CXBORDER);
rc.right += GetSystemMetrics(SM_CXBORDER) + 1;
rc.bottom += GetSystemMetrics(SM_CYBORDER) + 1;
// Create a layered window for transparency, with no caption/border.
CreateEx(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW, NULL, TEXT(""),
WS_POPUP, rc.left, rc.top, rc.Width(), rc.Height(),
pParent->GetSafeHwnd(), NULL);
// Bring in front of main window.
BringWindowToTop();
// Show the dimmer window
ShowWindow(SW_SHOW);
double increment_per_second = ((max_opacity - start_opacity) / fade_in_duration);
opacity_increment = ceil( increment_per_second / (ticks_per_second / rate) ) ;
is_dimmer_active = true;
opacity = start_opacity;
SetLayeredWindowAttributes(RGB(0,0,0), opacity, LWA_ALPHA);
SetTimer(TIMER_ID, rate,NULL);
}
}
CDimWnd::~CDimWnd()
{
fadeOut(); // fade the window out rather than just disappearing.
is_dimmer_active = false;
}
void CDimWnd::fadeOut()
{
// can't use timers as may be in the process of being destroyed so make it quick..
double increment_per_second = ((opacity - start_opacity) / fade_out_duration);
opacity_increment = ceil( increment_per_second / (ticks_per_second / rate) ) ;
while(opacity > start_opacity)
{
opacity -= opacity_increment;
SetLayeredWindowAttributes(RGB(0,0,0), opacity, LWA_ALPHA);
Sleep(100);
}
}
BOOL CDimWnd::OnEraseBkgnd(CDC* pDC)
{
// Fill with midgray
CBrush backBrush(RGB(128,128,128));
CBrush* pOldBrush = pDC->SelectObject(&backBrush);
CRect rect;
pDC->GetClipBox(&rect); // Erase the area needed
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY);
pDC->SelectObject(pOldBrush);
return TRUE;
}
BEGIN_MESSAGE_MAP(CDimWnd, CFrameWnd)
ON_WM_ERASEBKGND()
ON_WM_TIMER()
END_MESSAGE_MAP()
void CDimWnd::OnTimer(UINT_PTR nIDEvent)
{
if (opacity >= max_opacity)
{
// stop the timer when fade in finished.
KillTimer(TIMER_ID);
return;
}
opacity += opacity_increment;
SetLayeredWindowAttributes(RGB(0,0,0), opacity, LWA_ALPHA);
CFrameWnd::OnTimer(nIDEvent);
}