MFC - затемнить главное окно при отображении модального диалога - PullRequest
5 голосов
/ 31 октября 2010

У меня довольно стандартное приложение MFC, которое состоит из главного окна и иногда вызывает модальные диалоги. Как мы все знаем, ничего нельзя сделать вне модального диалога, пока он не будет закрыт.

Следовательно, хорошая функция пользовательского интерфейса состоит в том, чтобы «затемнить» остальную часть главного окна за диалоговым окном, чтобы визуально указать, что вы не можете использовать его, пока не закончите с модальным диалогом. Некоторые веб-приложения и java / mac-приложения делают это, но я никогда не видел, чтобы это делалось в традиционных приложениях C ++ / MFC. Я хотел бы попробовать, даже если это необычно для платформы.

Как это можно сделать? У меня есть несколько модальных диалогов в приложении, используемых в этом шаблоне:

// pMainFrame is available as a pointer to the CWnd of the main window
CMyDialog dialog;
dialog.DoModal(); // invoke modal dialog; returns after dialog closed

Есть ли простой способ затемнить окно перед любой DoModal () и затем восстановить? Я использую Visual Studio 2010 на случай, если в обновленном MFC есть какие-либо функции, которые могут помочь.

Редактировать: я опубликовал решение, основанное на ответе Ойстейна, но я начинаю вознаграждение на случай, если кто-то может улучшить его - особенно с плавным постепенным увеличением / исчезновением.

Ответы [ 3 ]

14 голосов
/ 02 ноября 2010

Вы можете создать другое окно, полностью черное, поверх окна, которое вы хотите затемнить, и установить непрозрачность черного окна с помощью SetLayeredWindowAttributes . Конечно, он не обязательно должен быть черным, но я думаю, что это лучший цвет для затемнения.

РЕДАКТИРОВАТЬ: я взломал пример - но учтите, что я не разработчик MFC, я обычно использую Windows API напрямую. Кажется, все работает хорошо, хотя. Здесь - это пастин. Не стесняйтесь добавлять постепенные изменения и т. Д. Самостоятельно. Также обратите внимание, что это затемняет весь экран, вам придется изменить размер моего окна затемнения, если вы не хотите этого поведения. Смотрите комментарии к коду.

/**********************************************************************************************

    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:

Я собрал еще немного кода для вас, чтобы справиться с исчезновением. Я до сих пор не являюсь разработчиком MFC, и я оставил код в «грубом» состоянии (небольшая обработка ошибок, не очень надежная), чтобы дать вам кое-что еще. :) Во всяком случае, вот один из способов сделать это, который я считаю достаточно чистым:

Чтобы использовать его, сделайте, чтобы ваше главное окно содержало окно диммера

class CMainFrm : public CFrameWnd
{     
    CDimWnd* dimmer; 

public: 
    CMainFrm()
    {
        // constructor code here ...
        dimmer = new CDimWnd();         
    }

// rest of class ...

};  

Затем его можно использовать, например, как это:

dimmer->Show();
MessageBox(TEXT("Hello world"));
dimmer->Hide();

В качестве альтернативы, я думаю, вы могли бы поместить этот код (Show() / Hide() вызовы) в конструктор и деструктор модального диалога, если вы хотите сохранить код там. Если вам нужен «scope» -dim, как в примере, который вы опубликовали, этот код должен был бы войти в конструктор и деструктор класса CDimWnd, и вам понадобится что-то вроде статической переменной-члена, чтобы гарантировать, что только один диммер выполняется одновременно (если вы не хотите использовать глобальную переменную).

Для окна диммера - я сделал это:

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;
}

Хорошо. Как я уже сказал, это было сделано довольно быстро и находится в грубом состоянии, но оно должно дать вам некоторый код для работы и общее представление о том, как (я думаю) таймеры используются в MFC. Я определенно не тот человек, чтобы думать об этом, хотя:)

3 голосов
/ 03 ноября 2010

Я принял ответ Ойстейна, так как он привел меня к решению, но я решил опубликовать свои модификации.Мне пришлось немного изменить его, чтобы он работал для меня, так что он может пригодиться кому-то еще.

Для справки, затемнение работает хорошо, но выглядит не так естественно, как я надеялся,В приложении, которое часто вызывает диалоги, затемнение начинает отвлекать своей регулярностью включения и выключения главного окна.Чтобы пойти на компромисс, я сделал затемнение довольно тонким (около 25% непрозрачности), которое мягко выделяет активный диалог;мгновенное затемнение все еще немного отвлекает, но я не уверен, как заставить его плавно исчезать или плавно исчезать, особенно когда он ограничен.

Кроме того, я не эксперт по пользовательскому интерфейсу, но затемнение далоу меня сложилось впечатление, что диалог был менее связан с содержимым окна позади него.Это заставило его чувствовать себя немного оторванным от того, над чем я работал в приложении, хотя диалоги напрямую манипулировали этим контентом.Это может быть еще одним отвлечением.

Вот оно в любом случае:

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;
}

Использование очень просто: поскольку CDimWnd создает себя в своем конструкторе , все, что вам нужно сделать, это добавить CDimWnd dimmer в качестве члена класса диалога, и он автоматически тускнеетглавное окно, независимо от того, откуда вы вызываете диалоговое окно.

Вы также можете использовать его в области видимости, чтобы затемнить модальные диалоговые окна системы:

{
    CDimWnd dimmer;
    MessageBox(...);
}
2 голосов
/ 05 ноября 2010

Я не мог удержаться от этого.

Это ваш код с добавленными таймерами и реализованным постепенным появлением / исчезновением. Также я изменил, чтобы использовать средний серый, а не черный для блока затенения.

Вы можете настроить константы, которые управляют затуханием, чтобы сделать его более плавным путем увеличения продолжительности или увеличения скорости. Эксперимент показывает мне, что частота 10 Гц является гладкой для меня, но 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);
}
...