天天看點

MFC無标題欄自繪對話框可拖動按鈕三态仿QQ彈窗樣式

    代碼提供下載下傳,想邊看程式源碼邊看文章的朋友直接去下載下傳:http://download.csdn.net/detail/panshiqu/5926325

    看我前幾天的bolg我實作了右下角彈窗功能,但是怎麼美化自己的彈窗呢,像騰訊QQ右下角彈騰訊大豫網新聞那樣的樣式。

    首先感謝一篇重要的文章:http://bbs.csdn.net/topics/390170722  作者:鄧學彬

    但是也需要指出的大凡已經很牛的人,對初學者的求助往往是漠不關心的,我對他文章的回複至今還沒有回我的問題,我給的留言也沒有收到回音,為什麼要感謝他,因為它用Win32(SDK)開發出來的程式,雖然我不想直接拿來用,但是它讓我堅信,假以時日我用MFC也可以實作的...

    直接上圖:

MFC無标題欄自繪對話框可拖動按鈕三态仿QQ彈窗樣式

    單文檔的美化我嘗試過,用的是重繪非客戶區的方法,遇到各種各樣的問題,一言難盡,現在我把這段時間美化對話框應用程式的經驗整理出來希望對初學者有點用。首先美化界面不靠其他界面庫的話,就要自己貼圖了,提到貼圖就要提一下在那個函數裡貼圖比較好,孫鑫VC++教程第10課的樣子,提到在OnPaint() 和 OnEraseBkgnd()兩個函數中繪制的差別,差別就是OnEraseBkgnd是直接網上貼圖,OnPaint貼圖之前則是先調用OnEraseBkgnd擦除背景,這樣一樣在OnEraseBkgnd中貼圖就會相對好些。一些沒有看過教程的朋友初次繪圖發現一切正常,怎麼就是不出繪圖效果呢,看一下MSDN對OnEraseBkgnd的介紹,“傳回非零如何擦除成功,否則是零值”。這樣你就要修改函數添加後的傳回值了。

return CDialogEx::OnEraseBkgnd(pDC);
           

改成

return TRUE;
           

說到貼圖不得不要提到三個函數:(請去看MSDN,示例看我的源代碼)

BOOL BitBlt(
   int x,
   int y,
   int nWidth,
   int nHeight,
   CDC* pSrcDC,
   int xSrc,
   int ySrc,
   DWORD dwRop 
);
           
BOOL StretchBlt(
   int x,
   int y,
   int nWidth,
   int nHeight,
   CDC* pSrcDC,
   int xSrc,
   int ySrc,
   int nSrcWidth,
   int nSrcHeight,
   DWORD dwRop 
);
           
BOOL TransparentBlt(
   int xDest,
   int yDest,
   int nDestWidth,
   int nDestHeight,
   CDC* pSrcDC,
   int xSrc,
   int ySrc,
   int nSrcWidth,
   int nSrcHeight,
   UINT clrTransparent 
);
           

用到這三個函數貼圖,貼圖的主體思想也是從鄧學斌那裡學來的,分成區域繪圖,這樣就不需要你的BMP皮膚檔案和你的程式一樣的大小了,發現貼圖貼的有模有樣了,突然發現我的程式不能拖動呀,這是肯定不行的呀,百度搜一下有解決辦法,響應OnNcHitTest() 函數就好了,網上很多文章有講,原理都有說的很清楚,先放上這段代碼看看程式是不是可以拖動了

UINT uRet = CDialog::OnNcHitTest(point);
return (HTCLIENT == uRet) ? HTCAPTION : uRet;
           

這樣還是沒有完成,按鈕三态變化還沒有實作,我就自定義一個成員函數如下

BOOL CDialogExDlg::OnChangeState(UINT control, UINT state)
{
    // control: 辨別那個按鈕
    // state: 按鈕目前變化狀态
    CClientDC dc(this);

    // 建立相容DC
    m_dcCompatible.CreateCompatibleDC(&dc);
    m_dcCompatible.SelectObject(&m_bitmapSkin);

    if (control)
    {
        // BMP檔案高度80出開始是檢視按鈕
        switch(state)
        {
        case 0:
            dc.TransparentBlt(m_nWndWidth-m_nMoreWidth-5, m_nWndHeight-m_nMoreHeight-5, m_nMoreWidth, m_nMoreHeight, 
                &m_dcCompatible, 0, 80, m_nMoreWidth, m_nMoreHeight, 0xFF00FF);
            // 滑鼠未點下不然在點這按鈕移出松開
            // 此時再放上去程式以為按鈕處于按下狀态
            m_bLButtonDown = false;
            break;
        case 1:
            dc.TransparentBlt(m_nWndWidth-m_nMoreWidth-5, m_nWndHeight-m_nMoreHeight-5, m_nMoreWidth, m_nMoreHeight, 
                &m_dcCompatible, m_nMoreWidth, 80, m_nMoreWidth, m_nMoreHeight, 0xFF00FF);
            // 滑鼠未點下不然在點這按鈕移出松開
            // 此時再放上去程式以為按鈕處于按下狀态
            m_bLButtonDown = false;
            break;
        case 2:
            dc.TransparentBlt(m_nWndWidth-m_nMoreWidth-5, m_nWndHeight-m_nMoreHeight-5, m_nMoreWidth, m_nMoreHeight, 
                &m_dcCompatible, 2*m_nMoreWidth, 80, m_nMoreWidth, m_nMoreHeight, 0xFF00FF);
            break;
        }
    } 
    else
    {
        // BMP檔案高度60出開始是關閉按鈕
        switch(state)
        {
        case 0:
            dc.TransparentBlt(m_nWndWidth-m_nCloseWidth, 1, m_nCloseWidth, m_nCloseHeight, 
                &m_dcCompatible, 0, 60, m_nCloseWidth, m_nCloseHeight, 0xFF00FF);
            // 滑鼠未點下不然在點這按鈕移出松開
            // 此時再放上去程式以為按鈕處于按下狀态
            m_bLButtonDown = false;
            break;
        case 1:
            dc.TransparentBlt(m_nWndWidth-m_nCloseWidth, 1, m_nCloseWidth, m_nCloseHeight, 
                &m_dcCompatible, m_nCloseWidth, 60, m_nCloseWidth, m_nCloseHeight, 0xFF00FF);
            // 滑鼠未點下不然在點這按鈕移出松開
            // 此時再放上去程式以為按鈕處于按下狀态
            m_bLButtonDown = false;
            break;
        case 2:
            dc.TransparentBlt(m_nWndWidth-m_nCloseWidth, 1, m_nCloseWidth, m_nCloseHeight, 
                &m_dcCompatible, 2*m_nCloseWidth, 60, m_nCloseWidth, m_nCloseHeight, 0xFF00FF);
            break;
        }
    }

    // 這裡最好删除一下
    m_dcCompatible.DeleteDC();

    return TRUE;
}
           

這個有了,就可以當滑鼠在按鈕上的時候,和滑鼠按下的時候進行不同的調用,來貼上不同的按鈕圖檔,但是重點來了,在那裡貼呢,腦子裡首先想到的是 OnMouseMove 函數裡,但是經過試驗,問題各種有,感覺就想已經有一個 OnNcHitTest 函數來捕獲滑鼠的操作輸入了,在用 OnMouseMove 就不靈的那種,各位朋友也可以做一下測試,也歡迎有發現的朋友和我讨論共同進步,話說回來 OnNcHitTest 本身可以實作的哦,我這樣寫的:

LRESULT CDialogExDlg::OnNcHitTest(CPoint point)
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值
	if (m_rectClose.PtInRect(point))
	{
		if (m_bLButtonDown)
		{
			// 關閉按鈕被按下
			OnChangeState(0, 2);
		}
		else
		{
			// 滑鼠在關閉按鈕上
			OnChangeState(0, 1);
		}
	}
	else if (m_rectMore.PtInRect(point))
	{
		if (m_bLButtonDown)
		{
			// 檢視按鈕被按下
			OnChangeState(1, 2);
		}
		else
		{
			// 滑鼠在檢視按鈕上
			OnChangeState(1, 1);
		}
	}
	else
	{
		// 關閉按鈕普通狀态
		OnChangeState(0, 0);

		// 檢視按鈕普通狀态
		OnChangeState(1, 0);

		// 移動對話框
		return HTCAPTION;
	}

	return CDialogEx::OnNcHitTest(point);
}
           

現在程式已經更加顯得有模有樣,接下來要賦予點選按鈕的操作,在OnLButtonDown()和OnLButtonUp()裡面來實作

void CDialogExDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值
	m_bLButtonDown = true;

	// 坐标轉換成螢幕坐标
	ClientToScreen(&point);

	if (m_rectClose.PtInRect(point))
	{
		// 關閉按鈕被按下
		OnChangeState(0, 2);

	}
	if (m_rectMore.PtInRect(point))
	{
		// 檢視按鈕被按下
		OnChangeState(1, 2);

	}
	CDialogEx::OnLButtonDown(nFlags, point);
}
           
void CDialogExDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值
	m_bLButtonDown = false;
	
	// 坐标轉換成螢幕坐标
	ClientToScreen(&point);

	if (m_rectClose.PtInRect(point))
	{
		CDialogEx::OnOK();
	}

	if (m_rectMore.PtInRect(point))
	{
		ShellExecute(NULL, _T("open"), _T("http://www.baidu.com"), NULL, NULL, SW_SHOWNORMAL);
		CDialogEx::OnOK();
	}

	CDialogEx::OnLButtonUp(nFlags, point);
}
           

看源碼看到 m_bLButtonDown 這個成員變量,其實注釋已經說的很清楚了,我說一個情況,大家不加這個變量控制看看是什麼情況,對話框彈出,我點關閉按鈕,突然發現内容我感興趣,滑鼠在關閉按鈕中移動一下,關閉按鈕的狀态就變成未按下的狀态了,如果僅在 Down Up 函數中指派 m_bLButtonDown 變量也有一個情況,點關閉按鈕,滑鼠移出松開滑鼠,程式沒有關閉,滑鼠再放上關閉按鈕,關閉按鈕直接顯示的就是滑鼠按下的狀态。大家可以試試哦。

程式都寫到這裡啦,可千萬不要出問題呀,可以問題還是出來了,我打開程式,移動了一下程式,去關閉程式,突然不能關閉,稍微一下就知道問題出到那裡了,我們是通過判斷滑鼠點選是不是在關閉按鈕的矩形區域内,在的話就退出程式,但是從程式啟動,如果不是界面需要重繪,OnEraseBkgnd 是不會再調用的,移動程式不會觸發界面重繪,強制界面重繪有函數的,但是我們不如重新計算關閉按鈕的矩形區域。那樣不是省事多了嗎,這個時候我就開始百度“程式拖動響應什麼函數”類似的關鍵字,沒有找到,沒有辦法,但是我覺得應該有函數,我想就借此機會看看都有那些消息,點了CDialogEx類視圖,點了消息,一個一個看了簡單的介紹,發現一個消息可能就是我想要的 WM_EXITSIZEMOVE VS2010上面的簡短介紹是:在視窗退出移動或大小調整模式循環後向視窗發送一次,添加一下,就是這個函數呵呵。

void CDialogExDlg::OnExitSizeMove()
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值

	// (更新)擷取視窗位置
	GetWindowRect(m_rectWnd);
	m_nWndWidth = m_rectWnd.Width();
	m_nWndHeight = m_rectWnd.Height();

	// 取關閉按鈕的矩形範圍(參照螢幕)
	m_rectClose.SetRect(m_nWndWidth-m_nCloseWidth, 1, m_nWndWidth, m_nCloseHeight+1);
	m_rectClose.OffsetRect(m_rectWnd.TopLeft().x, m_rectWnd.TopLeft().y); // 加上左上角坐标成為螢幕坐标

	// 取檢視按鈕的矩形範圍(參照螢幕)
	m_rectMore.SetRect(m_nWndWidth-m_nMoreWidth-5, m_nWndHeight-m_nMoreHeight-5, m_nWndWidth-5, m_nWndHeight-5);
	m_rectMore.OffsetRect(m_rectWnd.TopLeft().x, m_rectWnd.TopLeft().y); // 加上左上角坐标成為螢幕坐标

	CDialogEx::OnExitSizeMove();
}
           

我是追求完美的人,在測試的時候,發現關閉按鈕在按下的時候,滑鼠移出程式,隻要滑鼠不進入程式,關閉按鈕一直是被按下的狀态,這可如何是好,這個時候我看到了一篇文章:http://bbs.csdn.net/topics/120102520  雖然他是在論壇上提問他,但是他的問題對新人來說明顯幫助是很大的!但是我隻懂定時器,沒有辦法,硬着頭皮看了一下他的第二中方法,結合着搜尋了一下,又找到一篇文章:http://www.cnblogs.com/lzjsky/archive/2010/09/15/1826733.html  這下就可以下手了,其實我沒有看懂開頭他說的“在對話框類中定義一個變量來辨別是否追蹤目前滑鼠狀态,之是以要這樣定義是要避免滑鼠已經在窗體之上時,一移動滑鼠就不斷重複産生WM_MOUSEHOVER” 我還是按照我定義變量的風格(可能根本和這沒有關系)

void CDialogExDlg::OnMouseLeave()
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值

	// 再次允許追蹤滑鼠
	m_bMouseTrack = true;

	// 關閉按鈕普通狀态
	OnChangeState(0, 0);

	// 檢視按鈕普通狀态
	OnChangeState(1, 0);

	CDialogEx::OnMouseLeave();
}


void CDialogExDlg::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值
	if (m_bMouseTrack)    // 若允許追蹤則...
	{
		TRACKMOUSEEVENT csTME;
		csTME.cbSize = sizeof(csTME);
		csTME.dwFlags = TME_LEAVE|TME_HOVER;
		csTME.hwndTrack = m_hWnd;	// 指定要追蹤的視窗
		csTME.dwHoverTime = 10;		// 滑鼠在按鈕上停留超過10ms,才認為狀态為HOVER
		::_TrackMouseEvent(&csTME);	// 開啟Windows的WM_MOUSELEAVE,WM_MOUSEHOVER事件支援
		m_bMouseTrack = false;		//若已經追蹤,則停止追蹤
	}

	CDialogEx::OnMouseMove(nFlags, point);
}
           

這樣就搞定了,其實還是那句話,追求完美的我又發現了一個小問題,我點選關閉按鈕,滑鼠《猛》(一定要快)的移出程式視窗,停止操作,這是關閉按鈕就一直是按下的狀态了,直到滑鼠有動作才會退出按下狀态(這是的有動作就是在視窗外的動作也算,因為加了上面那兩個函數嗎呵呵)本來也想着看看能不能解決這個問題(因為發現騰訊的彈窗并沒有這個問題,我會說我這幾天每天都希望騰訊彈窗,彈出來都是不關的,好用來和我的程式比對)但是後來測試了鄧學斌的程式,也是有這個問題,我想還是算了吧,我還有一大堆工作要做呢,先隔一隔。

程式寫到這,終于算完了,拿給老闆看了一下樣式(他的電腦系統是XP,我的電腦系統是Win7)出現問題,問題還都是那些問題,Debug Assertion Failed 運作時出錯,我呢,就在我的虛拟機中運作了一下程式(虛拟機win2003),打開沒有問題,拖動出現問題,幸運的是我想到的和重新繪制有關,我菜鳥,排除類似這樣的問題就是重制個别功能,看在重寫的到那部分的時候會出問題,我這個程式,在剛一實作基本繪制的時候就出問題,我這個時候看到一個可以的對象。

BOOL CDialogExDlg::OnEraseBkgnd(CDC* pDC)
{
	// TODO: 在此添加消息處理程式代碼和/或調用預設值

	// 建立相容DC
	m_dcCompatible.CreateCompatibleDC(pDC);
	m_dcCompatible.SelectObject(&m_bitmapSkin);
           

我以前寫這個的都好像要删除,關閉之類的,但是,孫鑫老師第10課最後确實沒有提到,更重要的是 MSDN 中的例子都是這樣的,叫我情何以堪!!!

// This handler loads a bitmap from system resources,
// centers it in the view, and uses BitBlt() to paint the bitmap
// bits.
void CDCView::DrawBitmap(CDC* pDC)
{
   // load IDB_BITMAP1 from our resources
   CBitmap bmp;
   if (bmp.LoadBitmap(IDB_BITMAP1))
   {
      // Get the size of the bitmap
      BITMAP bmpInfo;
      bmp.GetBitmap(&bmpInfo);

      // Create an in-memory DC compatible with the
      // display DC we're using to paint
      CDC dcMemory;
      dcMemory.CreateCompatibleDC(pDC);

      // Select the bitmap into the in-memory DC
      CBitmap* pOldBitmap = dcMemory.SelectObject(&bmp);

      // Find a centerpoint for the bitmap in the client area
      CRect rect;
      GetClientRect(&rect);
      int nX = rect.left + (rect.Width() - bmpInfo.bmWidth) / 2;
      int nY = rect.top + (rect.Height() - bmpInfo.bmHeight) / 2;

      // Copy the bits from the in-memory DC into the on-
      // screen DC to actually do the painting. Use the centerpoint
      // we computed for the target offset.
      pDC->BitBlt(nX, nY, bmpInfo.bmWidth, bmpInfo.bmHeight, &dcMemory, 
         0, 0, SRCCOPY);

      dcMemory.SelectObject(pOldBitmap);
   }
   else
   {
      TRACE0("ERROR: Where's IDB_BITMAP1?\n");
   }
}
           

當我差不多快排除他沒有錯的時候,我嘗試性的加了一下 m_dcCompatible.DeleteDC(); 問題解決了,我去呀!!!

但是問題解決了,在我自定義的函數裡有用到 m_dcCompatible 呀,這是隻能在 OnChangeState() 裡重新建立相容DC啦,其實大家大可不必像我這樣,直接定義成普通變量也行的,因為随用随建立已經失去了成員變量的友善性,就沒有必要讓它是成員變量了

寫文章記錄的時候想想,也是呀,我看的是VS2010的MSDN,XP時估計VC++6.0吧,那時的MSDN可能例子有提到删除吧