I need to change the colors of the background, the text, the color of the dropdown button of
CDateTimeCtrl in MFC. I created new class derived from CDateTimeCtrl and tried overwritten OnCtlColor and CtlColor, but these functions are never called (the message map has been added using the Class Wizard). How should I achieve this without writing a completely new datetimectrl class of my own?
You can try something like this:
// header
CBrush* m_pBkgBrush { nullptr };
COLORREF m_bkgColor { RGB(255, 255, 255) };
COLORREF SetBackgroundColor(BOOL bSysColor, COLORREF cr);
and implementation now:
CYourDateTimeCtrl::CYourDateTimeCtrl()
: CDateTimeCtrl()
, m_bkgColor(::GetSysColor(COLOR_WINDOW))
{
m_pBkgBrush = new CBrush(::GetSysColor(COLOR_WINDOW));
}
CYourDateTimeCtrl::~CYourDateTimeCtrl()
{
if (nullptr != m_pBkgBrush)
delete m_pBkgBrush;
}
COLORREF CYourDateTimeCtrl::SetBackgroundColor(BOOL bSysColor, COLORREF cr)
{
COLORREF color = m_bkgColor;
m_bkgColor = bSysColor ? ::GetSysColor(COLOR_WINDOW) : cr;
if (color != m_bkgColor)
{
delete m_pBkgBrush;
m_pBkgBrush = new CBrush(m_bkgColor);
Invalidate();
}
return color;
}
BOOL CYourDateTimeCtrl::OnEraseBkgnd(CDC* pDC)
{
CBrush* pOldBrush = pDC->SelectObject(m_pBkgBrush);
CRect rect;
pDC->GetClipBox(&rect);
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATCOPY);
pDC->SelectObject(pOldBrush);
return TRUE;
}
When you use, you do:
MyMonthCalCtrl.SetBackgroundColor(FALSE, RGB(255, 255, 0));
I hope it helps.
Hello and happy new year, (it is acceptable to say it until Thursday)
I am trying to change the color of the tabs in the CTabCtrl class. I am trying to create my own ReskinCTablCtrl so that I can just call it in separate classes and easily use it throughout my program.
Currently I am able to change the background color of the CTabCtrl but I cannot modify the tab's themselves.
I used ON_WM_ERASEBKGND() for painting the background and it worked without a problem:
BOOL ReskinCTabCtrl::OnEraseBkgnd(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);
CBrush myBrush(RGB(51, 51, 51)); // dialog background color
BOOL bRes = pDC->PatBlt(0, 0, rect.Width(), rect.Height(), PATCOPY);
pDC->SetBkColor(RGB(51, 51, 51));
pDC->FillRect(&rect, &myBrush);
return bRes;
}
However, I have been unsuccesfull at changing the tab colors themselves. They are still the default MFC colors. I have tried to implement ON_WM_PAINT() and ON_WM_DRAWITEM() without any success. I think I can get to the specific tab rect with using both OnDraw and DrawItem similar to the second link example that I have posted in the end of this question.
void ReskinCTabCtrl::OnPaint() {
...
// paint the tabs first and then the borders
int nTab = GetItemCount();
int nSel = GetCurSel();
if (!nTab) // no pages added
return;
while (nTab--)
{
if (nTab != nSel)
{
dis.itemID = nTab;
dis.itemState = 0;
VERIFY(GetItemRect(nTab, &dis.rcItem));
dis.rcItem.bottom -= 2;
DrawItem(&dis);
DrawItemBorder(&dis);
}
}
...
}
I would really appreciate at least some direction to go about this problem, perhaps some more examples or what methods I should focus on using. I don't need the tabs to be different colors, maybe there is an easy way of doing this?
I've been trying to follow some examples like the following links but I still couldn't figure out the right way to do it.
https://support.microsoft.com/en-us/help/179909/how-to-change-the-background-color-of-a-tab-control
https://www.codeproject.com/Articles/1786/Ownerdraw-Tab-Controls-Borders-and-All
Enable OwnerDraw for tab control, either in resource editor, or set TCS_OWNERDRAWFIXED in OnInitDialog
CTabCtrl has message reflection for WM_DRAWITEM therefore we don't want to override WM_DRAWITEM/OnDrawItem from parent class. Instead override in CTabCtrl::DrawItem(LPDRAWITEMSTRUCT).
Unfortunately the result is rather ugly. It's sort of like overriding DrawItem in a button.
If Visual Style is available and enabled, then you can override CTabCtrl::OnPaint and draw everything manually. Example:
void CMyTabCtrl::OnPaint()
{
CPaintDC dc(this);
dc.SelectObject(GetFont());
CPen pen, pen_active;
COLORREF color_off = RGB(240, 240, 240);
COLORREF color_active = RGB(200, 240, 240);
CBrush brush_off, brush_active;
brush_off.CreateSolidBrush(color_off);
brush_active.CreateSolidBrush(color_active);
pen.CreatePen(PS_SOLID, 1, RGB(200, 200, 200));
pen_active.CreatePen(PS_SOLID, 1, color_active);
CRect rcitem;
GetItemRect(0, &rcitem);
CRect rc;
GetClientRect(&rc);
rc.bottom = rcitem.bottom;
dc.FillSolidRect(&rc, GetSysColor(COLOR_3DFACE));
GetClientRect(&rc);
rc.top = rcitem.bottom - 1;
dc.SelectObject(&pen);
dc.SelectObject(&brush_active);
dc.Rectangle(&rc);
for(int i = 0; i < GetItemCount(); i++)
{
dc.SelectObject(&pen);
if(i == GetCurSel())
{
dc.SelectObject(&brush_active);
dc.SetBkColor(color_active);
}
else
{
dc.SelectObject(&brush_off);
dc.SetBkColor(color_off);
}
GetItemRect(i, &rcitem);
rcitem.right++;
dc.Rectangle(&rcitem);
if(i == GetCurSel())
{
dc.SelectObject(pen_active);
dc.MoveTo(rcitem.left+1, rcitem.bottom - 1);
dc.LineTo(rcitem.right, rcitem.bottom - 1);
}
TCITEM item = { 0 };
wchar_t buf[32];
item.pszText = buf;
item.cchTextMax = 32;
item.mask = TCIF_TEXT;
GetItem(i, &item);
dc.DrawText(buf, &rcitem, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
}
BOOL CMyTabCtrl::OnEraseBkgnd(CDC*)
{
return TRUE;
}
I am trying to display a GIF picture as splash as starting of my small program in visual studio. I am really getting crazy. I looked it is possible in Qt IDE but I really need it in visual studio because my other code works only with visual studio. And yes I tried to convert my code for Qt giving me too many errors.
I have seen this post.
I am using GDI+ but still dunno how to simply display it instead of play and stop. It's okay even instead of splash to display a small form that plays the GIF file, can you guys give me a small code snippet in how to do it in c++?
Thanks.
Here is an MFC window class that implements a splash screen using GDI Plus for displaying an (animated) GIF.
I've encapsulated everything inside a header file to simplify using it with your project. Save it as an .h file (maybe "SplashWnd.h"), then include it wherever you want to set up the splash. You app's InitInstance might be a good place to add it - something like this line (before any DoModal calls):
SplashWnd splash(_T("Filname.gif"));
The constructor can also take parameters for controlling the delay before auto-closing, and also specifying a function to call when the splash is closed.
The splash window has no border or caption - it appears only as the loaded image, floating on top of any other windows. Its icon doesn't appear in the taskbar, and will close when its timeout expires, or the user clicks the window or presses a key.
#pragma once
#include <functional>
#include <afxwin.h>
#include <gdiplus.h>
#pragma comment(lib,"gdiplus.lib")
inline void ManageGdiPlusInit(bool release=false) {
static int refcount = 0;
static ULONG_PTR token;
if(release) {
if(--refcount == 0) {
Gdiplus::GdiplusShutdown(token);
}
} else if(++refcount == 1) {
Gdiplus::GdiplusStartupInput startup_input;
Gdiplus::GdiplusStartup(&token, &startup_input, 0);
} }
inline void GdiPlusInit() { ManageGdiPlusInit(false); }
inline void GdiPlusRelease() { ManageGdiPlusInit(true); }
namespace {
class SplashWnd : public CWnd {
protected:
static CString WindowClass() {
static CString name;
if(name.IsEmpty()) {
name = AfxRegisterWndClass(CS_DROPSHADOW, 0, (HBRUSH)GetStockObject(GRAY_BRUSH), 0);
}
return name;
}
Gdiplus::Image *m_pImage;
UINT m_FrameCount;
unsigned char *m_FrameDelayData;
const UINT *m_FrameDelays;
UINT m_CurFrameIndex;
UINT m_AnimationTimerId;
UINT m_ExpireTimerId;
CRect m_WindowRect;
std::function<void()> m_DismissCallback;
DECLARE_MESSAGE_MAP()
afx_msg void OnLButtonDown(UINT nFlags, CPoint point) {
DestroyWindow();
}
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
DestroyWindow();
}
afx_msg void OnDestroy() {
if(m_AnimationTimerId != UINT(-1)) {
KillTimer(m_AnimationTimerId);
}
if(m_ExpireTimerId != UINT(-1)) {
KillTimer(m_ExpireTimerId);
}
if(m_DismissCallback) {
m_DismissCallback();
}
CWnd::OnDestroy();
}
afx_msg void OnTimer(UINT nIDEvent) {
if(nIDEvent == m_AnimationTimerId) {
if(++m_CurFrameIndex >= m_FrameCount) {
m_CurFrameIndex = 0;
}
DrawCurFrame();
KillTimer(m_AnimationTimerId);
m_AnimationTimerId = SetTimer(1, m_FrameDelays[m_CurFrameIndex], 0);
return;
}
if(nIDEvent == m_ExpireTimerId) {
DestroyWindow();
return;
} }
void PostNcDestroy() {
if(m_DeleteSelf) {
delete this;
}
}
void DrawCurFrame() {
Gdiplus::Graphics g(m_hWnd);
GUID dim_select_id = Gdiplus::FrameDimensionTime;
m_pImage->SelectActiveFrame(&dim_select_id, m_CurFrameIndex);
g.DrawImage(m_pImage, 0, 0, m_WindowRect.Width(), m_WindowRect.Height());
}
public:
// set m_DeleteSelf to true if a SplashWnd is created with new, and you want it to
// auto-delete itself when the window expires or is dismissed.
bool m_DeleteSelf;
// file_path the gif file path
// ExpireMs the time, in milliseconds until the window automatically closes itself
// WidthFactor the fraction of the width of the primary display to use as the splash screen's width
// HeightFactor the fraction of the height of the primary display to use as the height
// If WidthFactor or HeightFactor are 0, the original image aspect ratio is preserved
// If both are 0, the original image size, in pixels is used
SplashWnd(CString file_path, DWORD ExpireMs=2000, double WidthFactor=0.4, double HeightFactor=0) {
GdiPlusInit();
m_pImage = new Gdiplus::Image(file_path);
// Set up an array of frame times for animated images
UINT dimension_count = m_pImage->GetFrameDimensionsCount();
GUID dimension_id;
m_pImage->GetFrameDimensionsList(&dimension_id, 1);
m_FrameCount = m_pImage->GetFrameCount(&dimension_id);
UINT frame_delay_size = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
m_FrameDelayData = new unsigned char[frame_delay_size];
Gdiplus::PropertyItem* frame_delay_item = reinterpret_cast<Gdiplus::PropertyItem*>(m_FrameDelayData);
m_pImage->GetPropertyItem(PropertyTagFrameDelay, frame_delay_size, frame_delay_item);
m_FrameDelays = reinterpret_cast<const UINT*>(frame_delay_item->value);
// Figure out the size and location of the splash window
int primary_width = GetSystemMetrics(SM_CXFULLSCREEN);
int primary_height = GetSystemMetrics(SM_CYFULLSCREEN);
int splash_width = int(primary_width * WidthFactor);
int splash_height = int(primary_height * HeightFactor);
if(splash_width == 0) {
if(splash_height == 0) {
splash_width = m_pImage->GetWidth();
splash_height = m_pImage->GetHeight();
} else {
splash_width = primary_width * splash_height / primary_height;
}
} else if(splash_height == 0) {
splash_height = primary_height * splash_width / primary_width;
}
int l = (primary_width - splash_width) / 2;
int t = (primary_height - splash_height) / 2;
int r = l + splash_width;
int b = t + splash_height;
m_WindowRect.SetRect(
(primary_width - splash_width) / 2,
(primary_height - splash_height) / 2,
(primary_width + splash_width) / 2,
(primary_height + splash_height) / 2);
// WS_EX_TOPMOST makes the window cover up other, regular windows
// WS_EX_TOOLWINDOW prevents an icon for this window in the taskbar
// WS_POPUP prevents caption and border from being drawn
CreateEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW, WindowClass(), _T("Splash"), WS_VISIBLE | WS_POPUP, m_WindowRect, 0, 0);
// Show the first frame
m_CurFrameIndex = 0;
DrawCurFrame();
// Set up the frame-flipping animation timer
m_ExpireTimerId = m_AnimationTimerId = UINT(-1);
if(m_FrameCount > 1) {
m_AnimationTimerId = SetTimer(1, m_FrameDelays[m_CurFrameIndex], 0);
}
// Set up the expiration timer
if(ExpireMs != INFINITE) {
m_ExpireTimerId = SetTimer(2, ExpireMs, 0);
}
m_DeleteSelf = false;
}
// Constructor which takes a callback function which will be called when the splash window closes
template <typename F>
SplashWnd(CString file_path, DWORD ExpireMs, double WidthFactor, double HeightFactor, F DismissCallback)
: SplashWnd(file_path, ExpireMs, WidthFactor, HeightFactor)
{
m_DismissCallback = DismissCallback;
}
~SplashWnd() {
delete [] m_FrameDelayData;
delete m_pImage;
GdiPlusRelease();
}
};
// Message map, usually in an implementation file, but here encapsulated inside the header
// using an anonymous namespace to prevent possible ODR problems.
BEGIN_MESSAGE_MAP(SplashWnd, CWnd)
ON_WM_KEYDOWN()
ON_WM_LBUTTONDOWN()
ON_WM_TIMER()
ON_WM_DESTROY()
END_MESSAGE_MAP()
}
I am trying to make a program currently that outputs a polygon to the desktop for a simple animation. The problem I am currently running into is that the animation gets an "onion" effect because the desktop isn't refreshing. I have searched for a method to refresh the desktop however because it's an animation, none of the solutions can refresh it fast enough. Below is an example of my code:
#include <iostream>
#include <Windows.h>
#include <math.h>
#include <Shlobj.h>
int main() {
//start ambrose
POINT amby[5];
POINT pos;
/* hide console window */
ShowWindow(FindWindowA("ConsoleWindowClass", NULL), false);
/* Calling GetDC with argument 0 retrieves the desktop's DC */
HDC hDC_Desktop = GetDC(0);
//This is just an example of what I am doing
for (int i = 0; i < 10000; i++) {
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
Sleep(10);
}
//The method I was trying before that didn't work VVVVV
//LPITEMIDLIST pidl;
//SHGetSpecialFolderLocation(NULL,CSIDL_DESKTOP,&pidl);
//SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,pidl,0);
return 0;
}
Thanks
Edit
I have tried using invalidateRect as such:
...
for (int i = 0; i < 10000; i++) {
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
InvalidateRect(GetDesktopWindow(),NULL, true);
Sleep(10);
}
...
I am wondering if there is anyway to call WM_ERASEBKGND or WM_DISPLAYCHANGE to force a change. Does anyone know if there is a way to call these?
I am not sure what you are trying to achieve. Let me just answer to problem of onion effect. A quick and dirty solution to erase what was drawn in the previous iteration could be to draw using XOR mode but the solution has a few downsides, like flicker and color could be arbitrary. A proper solution that would address both the downsides would be to do all the drawing in a memory DC and BitBlt the same to the screen.
Code for the quick and dirty solution would be -
SetROP2(hDC_Desktop,R2_XORPEN);
//This is just an example of what I am doing
for (int i = 0; i < 100; i++)
{
if(i!=0)
{
pos.x = 600+sin(double(i-1)/50)*200;
pos.y = 500+cos(double(i-1)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
}
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
Sleep(10);
}
There's an easy solution, and that's to not actually draw on the desktop. Instead, create a transparent full-screen window. Since it's transparent, any pixel that you don't draw will show the desktop underneath. Hence, only your polygon pixels will hide the underlying desktop.
As a result, the desktop window never needs to be invalidated or repainted etc.
Why don't you use a transparent wnd.
class COverlayWnd : public CWnd
{
DECLARE_DYNAMIC(COverlayWnd)
public:
COverlayWnd();
virtual ~COverlayWnd();
protected:
afx_msg void OnPaint();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
// OverlayWnd.cpp : implementation file
//
The implementation. Just move the window if you want animations to run all over the desktop.
#include "stdafx.h"
// COverlayWnd
IMPLEMENT_DYNAMIC(COverlayWnd, CWnd)
COverlayWnd::COverlayWnd()
{
}
COverlayWnd::~COverlayWnd()
{
}
BEGIN_MESSAGE_MAP(COverlayWnd, CWnd)
ON_WM_PAINT()
ON_WM_CREATE()
END_MESSAGE_MAP()
void COverlayWnd::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect( &rect );
dc.FillSolidRect(&rect, RGB(1,1,1));
//paint other stuff that don't have RGB(1,1,1)
}
int COverlayWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
BOOL bRet = 0;
bRet = ModifyStyleEx(0,WS_EX_LAYERED|WS_EX_TRANSPARENT);
bRet = ModifyStyle(DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU,0);
bRet = ModifyStyle(WS_POPUP,0);
bRet = SetLayeredWindowAttributes(RGB(1,1,1),0,LWA_COLORKEY);
//the RGB(1,1,1) is the transparent color
ASSERT(bRet);
//this->EnableWindow(FALSE);
return 0;
}
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);
}